862a9cdea0
This fixes the numbers by using the correct MIB to match the struct.
1437 lines
52 KiB
C
1437 lines
52 KiB
C
/* hbc_mini.c — HeartBeat Client, single-file C port of hbc_mini.py
|
|
*
|
|
* Build: cc -O2 -o hbc_mini hbc_mini.c -lz -lpthread -lm
|
|
* Requires: zlib, pthreads, POSIX
|
|
* Supported: Linux, FreeBSD, NetBSD, DragonFly BSD
|
|
*
|
|
* Drop on any host and run: ./hbc_mini <hbd-host>
|
|
* Config: ~/.hbc.json (same keys as Python version)
|
|
*/
|
|
|
|
#ifdef __linux__
|
|
# define _GNU_SOURCE
|
|
# define _POSIX_C_SOURCE 200809L
|
|
#endif
|
|
|
|
#include <arpa/inet.h>
|
|
#include <netinet/in.h>
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <math.h>
|
|
#include <netdb.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <stdarg.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/select.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
#include <sys/utsname.h>
|
|
#include <sys/wait.h>
|
|
#include <syslog.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <zlib.h>
|
|
|
|
#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
|
|
# include <sys/sysctl.h>
|
|
# include <net/if.h>
|
|
# include <net/if_dl.h>
|
|
# include <ifaddrs.h>
|
|
#endif
|
|
#if defined(__NetBSD__)
|
|
# include <uvm/uvm_extern.h>
|
|
#endif
|
|
|
|
/* version updated by scripts/bumpminor.sh */
|
|
#define HBC_VERSION "5.2.4"
|
|
#define DEFAULT_PORT 50003
|
|
#define DEFAULT_INTERVAL 10
|
|
#define MAX_HOSTS 8
|
|
#define MAX_KV 64
|
|
#define MAX_KEY 128
|
|
#define MAX_VAL 4096
|
|
#define PROTO_BUF_SIZE 65536
|
|
#define MAX_PING_HOSTS 32
|
|
#define MAX_NAGIOS_CMDS 32
|
|
#define MAX_DISK_MOUNTS 32
|
|
#define MAX_NET_SKIP 16
|
|
|
|
/* ============================================================
|
|
* Logging
|
|
* ============================================================ */
|
|
|
|
static bool g_use_syslog = false;
|
|
static int g_log_level = 3; /* 0=debug 1=info 2=warn 3=error */
|
|
|
|
#define LL_DEBUG 0
|
|
#define LL_INFO 1
|
|
#define LL_WARN 2
|
|
#define LL_ERROR 3
|
|
|
|
static void hbc_log(int level, const char *fmt, ...) {
|
|
if (level < g_log_level) return;
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
if (g_use_syslog) {
|
|
int pri = LOG_ERR;
|
|
if (level == LL_DEBUG) pri = LOG_DEBUG;
|
|
else if (level == LL_INFO) pri = LOG_INFO;
|
|
else if (level == LL_WARN) pri = LOG_WARNING;
|
|
vsyslog(pri, fmt, ap);
|
|
} else {
|
|
const char *ln[] = {"DEBUG","INFO","WARN","ERROR"};
|
|
time_t t = time(NULL);
|
|
struct tm tm; localtime_r(&t, &tm);
|
|
char ts[32]; strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm);
|
|
fprintf(stderr, "%s hbc %s: ", ts, ln[level]);
|
|
vfprintf(stderr, fmt, ap);
|
|
fputc('\n', stderr);
|
|
}
|
|
va_end(ap);
|
|
}
|
|
|
|
#define LOGD(fmt, ...) hbc_log(LL_DEBUG, fmt, ##__VA_ARGS__)
|
|
#define LOGI(fmt, ...) hbc_log(LL_INFO, fmt, ##__VA_ARGS__)
|
|
#define LOGW(fmt, ...) hbc_log(LL_WARN, fmt, ##__VA_ARGS__)
|
|
#define LOGE(fmt, ...) hbc_log(LL_ERROR, fmt, ##__VA_ARGS__)
|
|
|
|
/* ============================================================
|
|
* Key-Value dict (protocol messages)
|
|
* ============================================================ */
|
|
|
|
typedef struct { char key[MAX_KEY]; char val[MAX_VAL]; } kv_t;
|
|
typedef struct { kv_t pairs[MAX_KV]; int count; } kvdict_t;
|
|
|
|
static void kv_clear(kvdict_t *d) { d->count = 0; }
|
|
|
|
static bool kv_set(kvdict_t *d, const char *key, const char *val) {
|
|
if (d->count >= MAX_KV) return false;
|
|
snprintf(d->pairs[d->count].key, MAX_KEY, "%s", key);
|
|
snprintf(d->pairs[d->count].val, MAX_VAL, "%s", val);
|
|
d->count++;
|
|
return true;
|
|
}
|
|
|
|
static bool kv_set_int(kvdict_t *d, const char *key, long long v) {
|
|
char buf[32]; snprintf(buf, sizeof(buf), "%lld", v);
|
|
return kv_set(d, key, buf);
|
|
}
|
|
|
|
static bool kv_set_ull(kvdict_t *d, const char *key, unsigned long long v) {
|
|
char buf[32]; snprintf(buf, sizeof(buf), "%llu", v);
|
|
return kv_set(d, key, buf);
|
|
}
|
|
|
|
static bool kv_set_dbl(kvdict_t *d, const char *key, double v) {
|
|
char buf[32];
|
|
if (isinf(v)) snprintf(buf, sizeof(buf), "inf");
|
|
else snprintf(buf, sizeof(buf), "%.5f", v);
|
|
return kv_set(d, key, buf);
|
|
}
|
|
|
|
static const char *kv_get(const kvdict_t *d, const char *key) {
|
|
for (int i = 0; i < d->count; i++)
|
|
if (strcmp(d->pairs[i].key, key) == 0)
|
|
return d->pairs[i].val;
|
|
return NULL;
|
|
}
|
|
|
|
/* ============================================================
|
|
* Protocol codec (mirrors hbd/common/proto.py)
|
|
* ============================================================ */
|
|
|
|
/* Build wire bytes: !MSG: + zlib-compressed key=val;key=val;... */
|
|
static int proto_encode(const char *msg_id, const kvdict_t *d,
|
|
uint8_t *out, size_t cap) {
|
|
/* Build payload */
|
|
char *payload = malloc(PROTO_BUF_SIZE);
|
|
if (!payload) return -1;
|
|
int plen = 0;
|
|
for (int i = 0; i < d->count; i++) {
|
|
if (i > 0 && plen < PROTO_BUF_SIZE - 1) payload[plen++] = ';';
|
|
plen += snprintf(payload + plen, PROTO_BUF_SIZE - plen,
|
|
"%s=%s", d->pairs[i].key, d->pairs[i].val);
|
|
if (plen >= PROTO_BUF_SIZE) { plen = PROTO_BUF_SIZE - 1; break; }
|
|
}
|
|
/* Compress */
|
|
uLongf clen = compressBound(plen);
|
|
uint8_t *cbuf = malloc(clen);
|
|
if (!cbuf) { free(payload); return -1; }
|
|
if (compress2(cbuf, &clen, (uint8_t *)payload, plen, 6) != Z_OK) {
|
|
free(payload); free(cbuf); return -1;
|
|
}
|
|
free(payload);
|
|
/* Header: !XXX: (5 bytes) */
|
|
if ((size_t)(5 + clen) > cap) { free(cbuf); return -1; }
|
|
out[0] = '!';
|
|
memcpy(out + 1, msg_id, 3);
|
|
out[4] = ':';
|
|
memcpy(out + 5, cbuf, clen);
|
|
free(cbuf);
|
|
return 5 + (int)clen;
|
|
}
|
|
|
|
/* Parse wire bytes into kvdict */
|
|
static bool proto_decode(const uint8_t *data, int dlen, kvdict_t *out) {
|
|
kv_clear(out);
|
|
if (dlen < 5 || data[0] != '!') return false;
|
|
char id[4] = {0};
|
|
memcpy(id, data + 1, 3);
|
|
kv_set(out, "ID", id);
|
|
uint8_t *payload = malloc(PROTO_BUF_SIZE);
|
|
if (!payload) return false;
|
|
uLongf plen = PROTO_BUF_SIZE - 1;
|
|
if (uncompress(payload, &plen, data + 5, dlen - 5) != Z_OK) {
|
|
free(payload); return false;
|
|
}
|
|
payload[plen] = '\0';
|
|
char *p = (char *)payload;
|
|
while (*p) {
|
|
char *semi = strchr(p, ';');
|
|
if (semi) *semi = '\0';
|
|
char *eq = strchr(p, '=');
|
|
if (eq) {
|
|
*eq = '\0';
|
|
char *kp = p; while (*kp == ' ') kp++;
|
|
char *vp = eq+1; while (*vp == ' ') vp++;
|
|
kv_set(out, kp, vp);
|
|
}
|
|
if (semi) p = semi + 1; else break;
|
|
}
|
|
free(payload);
|
|
return true;
|
|
}
|
|
|
|
/* ============================================================
|
|
* Minimal JSON parser (for config file)
|
|
* ============================================================ */
|
|
|
|
typedef enum { JT_NULL,JT_BOOL,JT_INT,JT_FLOAT,JT_STRING,JT_ARRAY,JT_OBJECT } jtype_t;
|
|
|
|
typedef struct jval jval_t;
|
|
struct jval {
|
|
jtype_t type;
|
|
union {
|
|
bool b;
|
|
long long i;
|
|
double f;
|
|
char *s;
|
|
struct { char **keys; jval_t **vals; int count, cap; } obj;
|
|
struct { jval_t **items; int count, cap; } arr;
|
|
};
|
|
};
|
|
|
|
static void jval_free(jval_t *v) {
|
|
if (!v) return;
|
|
if (v->type == JT_STRING) { free(v->s); }
|
|
else if (v->type == JT_OBJECT) {
|
|
for (int i = 0; i < v->obj.count; i++) { free(v->obj.keys[i]); jval_free(v->obj.vals[i]); }
|
|
free(v->obj.keys); free(v->obj.vals);
|
|
} else if (v->type == JT_ARRAY) {
|
|
for (int i = 0; i < v->arr.count; i++) jval_free(v->arr.items[i]);
|
|
free(v->arr.items);
|
|
}
|
|
free(v);
|
|
}
|
|
|
|
static const char *jskip(const char *p) {
|
|
while (*p && isspace((unsigned char)*p)) p++;
|
|
return p;
|
|
}
|
|
|
|
static const char *jparse_value(const char *p, jval_t **out);
|
|
|
|
static const char *jparse_string(const char *p, char **out) {
|
|
if (*p != '"') return NULL;
|
|
p++;
|
|
char buf[4096]; int n = 0;
|
|
while (*p && *p != '"') {
|
|
if (*p == '\\') {
|
|
p++;
|
|
switch (*p) {
|
|
case '"': buf[n++]='"'; break; case '\\': buf[n++]='\\'; break;
|
|
case 'n': buf[n++]='\n'; break; case 'r': buf[n++]='\r'; break;
|
|
case 't': buf[n++]='\t'; break; default: buf[n++]=*p; break;
|
|
}
|
|
} else { buf[n++] = *p; }
|
|
p++;
|
|
if (n >= (int)sizeof(buf)-1) break;
|
|
}
|
|
buf[n] = '\0';
|
|
if (*p == '"') p++;
|
|
*out = strdup(buf);
|
|
return p;
|
|
}
|
|
|
|
static const char *jparse_object(const char *p, jval_t **out) {
|
|
if (*p != '{') return NULL;
|
|
p++;
|
|
jval_t *v = calloc(1, sizeof(jval_t)); v->type = JT_OBJECT;
|
|
p = jskip(p);
|
|
while (*p && *p != '}') {
|
|
p = jskip(p);
|
|
char *key = NULL;
|
|
p = jparse_string(p, &key);
|
|
if (!p) break;
|
|
p = jskip(p); if (*p == ':') p++;
|
|
p = jskip(p);
|
|
jval_t *val = NULL;
|
|
p = jparse_value(p, &val);
|
|
if (key && val) {
|
|
if (v->obj.count >= v->obj.cap) {
|
|
v->obj.cap = v->obj.cap ? v->obj.cap*2 : 4;
|
|
v->obj.keys = realloc(v->obj.keys, v->obj.cap * sizeof(char*));
|
|
v->obj.vals = realloc(v->obj.vals, v->obj.cap * sizeof(jval_t*));
|
|
}
|
|
v->obj.keys[v->obj.count] = key;
|
|
v->obj.vals[v->obj.count] = val;
|
|
v->obj.count++;
|
|
} else { free(key); jval_free(val); }
|
|
p = jskip(p); if (*p == ',') p++;
|
|
p = jskip(p);
|
|
}
|
|
if (*p == '}') p++;
|
|
*out = v; return p;
|
|
}
|
|
|
|
static const char *jparse_array(const char *p, jval_t **out) {
|
|
if (*p != '[') return NULL;
|
|
p++;
|
|
jval_t *v = calloc(1, sizeof(jval_t)); v->type = JT_ARRAY;
|
|
p = jskip(p);
|
|
while (*p && *p != ']') {
|
|
jval_t *item = NULL;
|
|
p = jparse_value(jskip(p), &item);
|
|
if (item) {
|
|
if (v->arr.count >= v->arr.cap) {
|
|
v->arr.cap = v->arr.cap ? v->arr.cap*2 : 4;
|
|
v->arr.items = realloc(v->arr.items, v->arr.cap * sizeof(jval_t*));
|
|
}
|
|
v->arr.items[v->arr.count++] = item;
|
|
}
|
|
p = jskip(p); if (*p == ',') p++;
|
|
}
|
|
if (*p == ']') p++;
|
|
*out = v; return p;
|
|
}
|
|
|
|
static const char *jparse_value(const char *p, jval_t **out) {
|
|
p = jskip(p);
|
|
if (!*p) { *out = NULL; return p; }
|
|
if (*p == '{') return jparse_object(p, out);
|
|
if (*p == '[') return jparse_array(p, out);
|
|
if (*p == '"') {
|
|
char *s = NULL; p = jparse_string(p, &s);
|
|
jval_t *v = calloc(1, sizeof(jval_t)); v->type = JT_STRING; v->s = s;
|
|
*out = v; return p;
|
|
}
|
|
if (strncmp(p,"true",4)==0) { jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_BOOL; v->b=true; *out=v; return p+4; }
|
|
if (strncmp(p,"false",5)==0) { jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_BOOL; v->b=false; *out=v; return p+5; }
|
|
if (strncmp(p,"null",4)==0) { jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_NULL; *out=v; return p+4; }
|
|
if (isdigit((unsigned char)*p) || *p == '-') {
|
|
char *e1, *e2;
|
|
long long iv = strtoll(p, &e1, 10);
|
|
double fv = strtod(p, &e2);
|
|
if (e2 > e1) {
|
|
jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_FLOAT; v->f=fv; *out=v; return e2;
|
|
} else {
|
|
jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_INT; v->i=iv; *out=v; return e1;
|
|
}
|
|
}
|
|
*out = NULL; return p + 1;
|
|
}
|
|
|
|
static jval_t *json_parse(const char *src) {
|
|
jval_t *v = NULL; jparse_value(src, &v); return v;
|
|
}
|
|
|
|
static jval_t *jget(const jval_t *obj, const char *key) {
|
|
if (!obj || obj->type != JT_OBJECT) return NULL;
|
|
for (int i = 0; i < obj->obj.count; i++)
|
|
if (strcmp(obj->obj.keys[i], key) == 0) return obj->obj.vals[i];
|
|
return NULL;
|
|
}
|
|
|
|
static long long jint(const jval_t *v, long long def) {
|
|
if (!v) return def;
|
|
if (v->type==JT_INT) return v->i;
|
|
if (v->type==JT_FLOAT) return (long long)v->f;
|
|
if (v->type==JT_BOOL) return v->b ? 1 : 0;
|
|
return def;
|
|
}
|
|
static const char *jstr(const jval_t *v, const char *def) {
|
|
return (!v || v->type!=JT_STRING) ? def : v->s;
|
|
}
|
|
|
|
/* ============================================================
|
|
* Config
|
|
* ============================================================ */
|
|
|
|
typedef struct {
|
|
int hb_port, interval;
|
|
char owner[256];
|
|
|
|
char ping_hosts[MAX_PING_HOSTS][256];
|
|
int ping_host_count, ping_interval, ping_count, ping_timeout;
|
|
|
|
struct { char name[64]; char cmd[512]; } nagios[MAX_NAGIOS_CMDS];
|
|
int nagios_count, nagios_interval, nagios_timeout;
|
|
|
|
int cpu_interval, mem_interval, disk_interval, net_interval;
|
|
|
|
char disk_mounts[MAX_DISK_MOUNTS][256];
|
|
int disk_mount_count;
|
|
|
|
char net_skip[MAX_NET_SKIP][64];
|
|
int net_skip_count;
|
|
} config_t;
|
|
|
|
static void config_defaults(config_t *c) {
|
|
memset(c, 0, sizeof(*c));
|
|
c->hb_port = DEFAULT_PORT;
|
|
c->interval = DEFAULT_INTERVAL;
|
|
c->ping_interval = 60; c->ping_count = 3; c->ping_timeout = 5;
|
|
c->nagios_interval = 300; c->nagios_timeout = 30;
|
|
c->cpu_interval = 300;
|
|
c->mem_interval = 300;
|
|
c->disk_interval = 300;
|
|
c->net_interval = 300;
|
|
snprintf(c->net_skip[0], 64, "lo");
|
|
c->net_skip_count = 1;
|
|
}
|
|
|
|
static void config_load(config_t *cfg, const char *path) {
|
|
config_defaults(cfg);
|
|
char fpath[512];
|
|
if (!path) {
|
|
const char *home = getenv("HOME");
|
|
snprintf(fpath, sizeof(fpath), "%s/.hbc.json", home ? home : "");
|
|
path = fpath;
|
|
}
|
|
FILE *f = fopen(path, "r");
|
|
if (!f) return;
|
|
fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET);
|
|
char *buf = malloc(sz + 1);
|
|
if (!buf) { fclose(f); return; }
|
|
size_t _nr = fread(buf, 1, sz, f); buf[_nr] = '\0'; fclose(f);
|
|
jval_t *root = json_parse(buf); free(buf);
|
|
if (!root || root->type != JT_OBJECT) { LOGW("cannot parse %s", path); jval_free(root); return; }
|
|
|
|
jval_t *v;
|
|
if ((v = jget(root, "hb_port"))) cfg->hb_port = jint(v, cfg->hb_port);
|
|
if ((v = jget(root, "interval"))) cfg->interval = jint(v, cfg->interval);
|
|
if ((v = jget(root, "owner"))) snprintf(cfg->owner, sizeof(cfg->owner), "%s", jstr(v, ""));
|
|
|
|
jval_t *plugins = jget(root, "plugins");
|
|
|
|
/* ping_monitor */
|
|
jval_t *pm = plugins ? jget(plugins,"ping_monitor") : jget(root,"ping_monitor");
|
|
if (pm && pm->type == JT_OBJECT) {
|
|
if ((v=jget(pm,"interval"))) cfg->ping_interval = jint(v, cfg->ping_interval);
|
|
if ((v=jget(pm,"count"))) cfg->ping_count = jint(v, cfg->ping_count);
|
|
if ((v=jget(pm,"timeout"))) cfg->ping_timeout = jint(v, cfg->ping_timeout);
|
|
jval_t *h = jget(pm, "hosts");
|
|
if (h && h->type == JT_ARRAY) {
|
|
for (int i = 0; i < h->arr.count && cfg->ping_host_count < MAX_PING_HOSTS; i++) {
|
|
const char *s = jstr(h->arr.items[i], NULL);
|
|
if (s) snprintf(cfg->ping_hosts[cfg->ping_host_count++], 256, "%s", s);
|
|
}
|
|
} else if (h && h->type == JT_OBJECT) {
|
|
for (int i = 0; i < h->obj.count && cfg->ping_host_count < MAX_PING_HOSTS; i++)
|
|
snprintf(cfg->ping_hosts[cfg->ping_host_count++], 256, "%s", h->obj.keys[i]);
|
|
}
|
|
}
|
|
|
|
/* nagios_runner */
|
|
jval_t *nr = plugins ? jget(plugins,"nagios_runner") : jget(root,"nagios_runner");
|
|
if (nr && nr->type == JT_OBJECT) {
|
|
if ((v=jget(nr,"interval"))) cfg->nagios_interval = jint(v, cfg->nagios_interval);
|
|
if ((v=jget(nr,"timeout"))) cfg->nagios_timeout = jint(v, cfg->nagios_timeout);
|
|
jval_t *cmds = jget(nr, "commands");
|
|
if (cmds && cmds->type == JT_ARRAY) {
|
|
for (int i = 0; i < cmds->arr.count && cfg->nagios_count < MAX_NAGIOS_CMDS; i++) {
|
|
jval_t *c = cmds->arr.items[i];
|
|
if (!c || c->type != JT_OBJECT) continue;
|
|
jval_t *n = jget(c,"name"), *cmd = jget(c,"command");
|
|
if (n && cmd) {
|
|
int idx = cfg->nagios_count++;
|
|
snprintf(cfg->nagios[idx].name, 64, "%s", jstr(n, ""));
|
|
snprintf(cfg->nagios[idx].cmd, 512, "%s", jstr(cmd, ""));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* cpu/memory/disk/network */
|
|
jval_t *cpu = plugins ? jget(plugins,"cpu_monitor") : jget(root,"cpu_monitor");
|
|
if (cpu && (v=jget(cpu,"interval"))) cfg->cpu_interval = jint(v, cfg->cpu_interval);
|
|
|
|
jval_t *mem = plugins ? jget(plugins,"memory_monitor") : jget(root,"memory_monitor");
|
|
if (mem && (v=jget(mem,"interval"))) cfg->mem_interval = jint(v, cfg->mem_interval);
|
|
|
|
jval_t *disk = plugins ? jget(plugins,"disk_monitor") : jget(root,"disk_monitor");
|
|
if (disk && disk->type == JT_OBJECT) {
|
|
if ((v=jget(disk,"interval"))) cfg->disk_interval = jint(v, cfg->disk_interval);
|
|
jval_t *m = jget(disk,"mounts");
|
|
if (m && m->type == JT_ARRAY)
|
|
for (int i = 0; i < m->arr.count && cfg->disk_mount_count < MAX_DISK_MOUNTS; i++) {
|
|
const char *s = jstr(m->arr.items[i], NULL);
|
|
if (s) snprintf(cfg->disk_mounts[cfg->disk_mount_count++], 256, "%s", s);
|
|
}
|
|
}
|
|
|
|
jval_t *net = plugins ? jget(plugins,"network_monitor") : jget(root,"network_monitor");
|
|
if (net && net->type == JT_OBJECT) {
|
|
if ((v=jget(net,"interval"))) cfg->net_interval = jint(v, cfg->net_interval);
|
|
jval_t *sk = jget(net,"skip_interfaces");
|
|
if (sk && sk->type == JT_ARRAY) {
|
|
cfg->net_skip_count = 0;
|
|
for (int i = 0; i < sk->arr.count && cfg->net_skip_count < MAX_NET_SKIP; i++) {
|
|
const char *s = jstr(sk->arr.items[i], NULL);
|
|
if (s) snprintf(cfg->net_skip[cfg->net_skip_count++], 64, "%s", s);
|
|
}
|
|
}
|
|
}
|
|
|
|
LOGI("loaded config from %s", path);
|
|
jval_free(root);
|
|
}
|
|
|
|
/* ============================================================
|
|
* UDP connection
|
|
* ============================================================ */
|
|
|
|
typedef struct {
|
|
int conn_id;
|
|
char addr[INET6_ADDRSTRLEN];
|
|
int port, af, sockfd;
|
|
char name[256];
|
|
long long ack_count;
|
|
double last_send, rtt;
|
|
volatile bool request_info;
|
|
pthread_mutex_t mu;
|
|
} conn_t;
|
|
|
|
static double now_ts(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_REALTIME, &ts);
|
|
return ts.tv_sec + ts.tv_nsec * 1e-9;
|
|
}
|
|
|
|
static bool conn_open(conn_t *c) {
|
|
c->sockfd = socket(c->af, SOCK_DGRAM, 0);
|
|
if (c->sockfd < 0) { LOGE("socket: %s", strerror(errno)); return false; }
|
|
int fl = fcntl(c->sockfd, F_GETFL, 0);
|
|
fcntl(c->sockfd, F_SETFL, fl | O_NONBLOCK);
|
|
pthread_mutex_init(&c->mu, NULL);
|
|
return true;
|
|
}
|
|
|
|
static void conn_close(conn_t *c) {
|
|
if (c->sockfd >= 0) { close(c->sockfd); c->sockfd = -1; }
|
|
}
|
|
|
|
static int conn_send(conn_t *c, const char *msg_id, kvdict_t *d) {
|
|
kv_set(d, "name", c->name);
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "%d", c->conn_id); kv_set(d, "id", buf);
|
|
snprintf(buf, sizeof(buf), "%.3f", now_ts()); kv_set(d, "time", buf);
|
|
|
|
uint8_t wire[PROTO_BUF_SIZE];
|
|
int wlen = proto_encode(msg_id, d, wire, sizeof(wire));
|
|
if (wlen < 0) { LOGW("proto_encode failed"); return -1; }
|
|
|
|
struct sockaddr_storage sa; socklen_t salen = 0;
|
|
memset(&sa, 0, sizeof(sa));
|
|
if (c->af == AF_INET) {
|
|
struct sockaddr_in *s4 = (struct sockaddr_in *)&sa;
|
|
s4->sin_family = AF_INET;
|
|
s4->sin_port = htons(c->port);
|
|
inet_pton(AF_INET, c->addr, &s4->sin_addr);
|
|
salen = sizeof(*s4);
|
|
} else {
|
|
struct sockaddr_in6 *s6 = (struct sockaddr_in6 *)&sa;
|
|
s6->sin6_family = AF_INET6;
|
|
s6->sin6_port = htons(c->port);
|
|
inet_pton(AF_INET6, c->addr, &s6->sin6_addr);
|
|
salen = sizeof(*s6);
|
|
}
|
|
|
|
pthread_mutex_lock(&c->mu);
|
|
c->last_send = now_ts();
|
|
ssize_t sent = sendto(c->sockfd, wire, wlen, 0, (struct sockaddr *)&sa, salen);
|
|
pthread_mutex_unlock(&c->mu);
|
|
return (sent == wlen) ? 0 : -1;
|
|
}
|
|
|
|
static void conn_recv(conn_t *c) {
|
|
uint8_t buf[PROTO_BUF_SIZE];
|
|
struct sockaddr_storage sa; socklen_t sl = sizeof(sa);
|
|
ssize_t n = recvfrom(c->sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&sa, &sl);
|
|
if (n <= 0) return;
|
|
|
|
kvdict_t msg;
|
|
if (!proto_decode(buf, (int)n, &msg)) return;
|
|
const char *id = kv_get(&msg, "ID");
|
|
if (!id) return;
|
|
|
|
if (strcmp(id, "ACK") == 0) {
|
|
c->rtt = (now_ts() - c->last_send) * 1000.0;
|
|
c->ack_count++;
|
|
const char *ru = kv_get(&msg, "request_update");
|
|
if (ru && strcmp(ru,"1") == 0) c->request_info = true;
|
|
LOGD("ACK rtt=%.1fms", c->rtt);
|
|
} else if (strcmp(id, "CMD") == 0) {
|
|
const char *cmd = kv_get(&msg, "cmd");
|
|
if (cmd) {
|
|
LOGI("CMD: %s", cmd);
|
|
char out[4096] = "";
|
|
FILE *p = popen(cmd, "r");
|
|
if (p) { size_t _r = fread(out, 1, sizeof(out)-1, p); (void)_r; pclose(p); }
|
|
kvdict_t rep; kv_clear(&rep);
|
|
kv_set(&rep, "service", "command");
|
|
kv_set(&rep, "msg", out[0] ? out : "OK");
|
|
conn_send(c, "HTB", &rep);
|
|
}
|
|
} else if (strcmp(id, "UPD") == 0) {
|
|
LOGI("UPD: update requested (not implemented in C version)");
|
|
}
|
|
}
|
|
|
|
/* ============================================================
|
|
* Global state
|
|
* ============================================================ */
|
|
|
|
static volatile bool g_running = true;
|
|
static volatile bool g_restart = false;
|
|
static conn_t g_conns[MAX_HOSTS];
|
|
static int g_nconns = 0;
|
|
|
|
static void sig_handler(int sig) {
|
|
if (sig == SIGHUP) g_restart = true;
|
|
g_running = false;
|
|
}
|
|
|
|
/* ============================================================
|
|
* Plugin: os_info
|
|
* ============================================================ */
|
|
|
|
static void plugin_os_info(conn_t *c, const config_t *cfg) {
|
|
kvdict_t d; kv_clear(&d);
|
|
kv_set(&d, "plugin", "os_info");
|
|
struct utsname u;
|
|
if (uname(&u) == 0) {
|
|
kv_set(&d, "system", u.sysname);
|
|
kv_set(&d, "node", u.nodename);
|
|
kv_set(&d, "release", u.release);
|
|
kv_set(&d, "machine", u.machine);
|
|
}
|
|
kv_set(&d, "hbc_version", HBC_VERSION);
|
|
kv_set(&d, "hbc_type", "mini-c");
|
|
if (cfg->owner[0]) kv_set(&d, "owner", cfg->owner);
|
|
|
|
#ifdef __linux__
|
|
{
|
|
FILE *f = fopen("/etc/os-release", "r");
|
|
if (!f) f = fopen("/etc/lsb-release", "r");
|
|
if (f) {
|
|
char line[512];
|
|
while (fgets(line, sizeof(line), f)) {
|
|
char *eq = strchr(line, '=');
|
|
if (!eq || line[0] == '#') continue;
|
|
*eq = '\0';
|
|
char *val = eq + 1;
|
|
int vl = strlen(val);
|
|
while (vl > 0 && (val[vl-1]=='\n'||val[vl-1]=='\r'||val[vl-1]=='"'||val[vl-1]=='\'')) val[--vl]='\0';
|
|
if (*val == '"' || *val == '\'') val++;
|
|
if (strcmp(line,"PRETTY_NAME")==0) kv_set(&d, "distro_pretty_name", val);
|
|
else if (strcmp(line,"NAME")==0) kv_set(&d, "distro_name", val);
|
|
else if (strcmp(line,"VERSION_ID")==0) kv_set(&d, "distro_version_id", val);
|
|
else if (strcmp(line,"ID")==0) kv_set(&d, "distro_id", val);
|
|
}
|
|
fclose(f);
|
|
}
|
|
}
|
|
#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
|
|
{
|
|
char osver[128] = ""; size_t len = sizeof(osver);
|
|
sysctlbyname("kern.osrelease", osver, &len, NULL, 0);
|
|
if (osver[0]) kv_set(&d, "distro_version_id", osver);
|
|
}
|
|
#endif
|
|
conn_send(c, "PLG", &d);
|
|
LOGI("sent os_info");
|
|
}
|
|
|
|
/* ============================================================
|
|
* Plugin: cpu_monitor
|
|
* Linux: /proc/stat (user nice sys idle iowait irq softirq steal)
|
|
* BSD: sysctl kern.cp_time (CP_USER CP_NICE CP_SYS CP_INTR CP_IDLE)
|
|
* ============================================================ */
|
|
|
|
typedef struct {
|
|
long long user, sys, idle, iowait, total;
|
|
} cpu_sample_t;
|
|
|
|
#ifdef __linux__
|
|
|
|
static bool read_cpu_sample(cpu_sample_t *s) {
|
|
FILE *f = fopen("/proc/stat", "r");
|
|
if (!f) return false;
|
|
char line[256]; bool ok = false;
|
|
while (fgets(line, sizeof(line), f)) {
|
|
if (strncmp(line, "cpu ", 4) != 0) continue;
|
|
long long u=0, ni=0, sy=0, id=0, iow=0, irq=0, sirq=0;
|
|
int n = sscanf(line+4, "%lld %lld %lld %lld %lld %lld %lld",
|
|
&u, &ni, &sy, &id, &iow, &irq, &sirq);
|
|
if (n < 4) break;
|
|
s->user = u + ni;
|
|
s->sys = sy + irq + sirq;
|
|
s->idle = id;
|
|
s->iowait = (n >= 5) ? iow : 0;
|
|
s->total = u + ni + sy + id + iow + irq + sirq;
|
|
ok = true; break;
|
|
}
|
|
fclose(f); return ok;
|
|
}
|
|
|
|
static void read_cpu_extras(kvdict_t *d) {
|
|
FILE *fi = fopen("/proc/cpuinfo", "r");
|
|
if (fi) {
|
|
char ln[256]; int cores = 0;
|
|
while (fgets(ln, sizeof(ln), fi)) if (strncmp(ln,"processor",9)==0) cores++;
|
|
fclose(fi);
|
|
if (cores > 0) kv_set_int(d, "cpu_core_count", cores);
|
|
}
|
|
fi = fopen("/proc/uptime", "r");
|
|
if (fi) {
|
|
double up = 0;
|
|
if (fscanf(fi, "%lf", &up) == 1) kv_set_int(d, "uptime_seconds", (long long)up);
|
|
fclose(fi);
|
|
}
|
|
}
|
|
|
|
#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
|
|
|
|
static bool read_cpu_sample(cpu_sample_t *s) {
|
|
/* kern.cp_time: CP_USER(0) CP_NICE(1) CP_SYS(2) CP_INTR(3) CP_IDLE(4) */
|
|
unsigned long cp[5] = {0};
|
|
size_t len = sizeof(cp);
|
|
if (sysctlbyname("kern.cp_time", cp, &len, NULL, 0) != 0) return false;
|
|
s->user = (long long)cp[0] + (long long)cp[1];
|
|
s->sys = (long long)cp[2] + (long long)cp[3]; /* SYS + INTR */
|
|
s->idle = (long long)cp[4];
|
|
s->iowait = 0;
|
|
s->total = s->user + (long long)cp[2] + (long long)cp[3] + s->idle;
|
|
return true;
|
|
}
|
|
|
|
static void read_cpu_extras(kvdict_t *d) {
|
|
int ncpu = 0; size_t len = sizeof(ncpu);
|
|
if (sysctlbyname("hw.ncpu", &ncpu, &len, NULL, 0) == 0 && ncpu > 0)
|
|
kv_set_int(d, "cpu_core_count", ncpu);
|
|
struct timeval bt; len = sizeof(bt);
|
|
if (sysctlbyname("kern.boottime", &bt, &len, NULL, 0) == 0)
|
|
kv_set_int(d, "uptime_seconds", (long long)(time(NULL) - bt.tv_sec));
|
|
}
|
|
|
|
#endif /* platform cpu */
|
|
|
|
static cpu_sample_t g_cpu_prev;
|
|
static bool g_cpu_have_prev = false;
|
|
|
|
static void plugin_cpu_monitor(conn_t *c, const config_t *cfg) {
|
|
(void)cfg;
|
|
cpu_sample_t t1, t2;
|
|
if (!g_cpu_have_prev) {
|
|
if (!read_cpu_sample(&t1)) return;
|
|
sleep(1);
|
|
if (!read_cpu_sample(&t2)) return;
|
|
} else {
|
|
t1 = g_cpu_prev;
|
|
if (!read_cpu_sample(&t2)) return;
|
|
}
|
|
g_cpu_prev = t2; g_cpu_have_prev = true;
|
|
|
|
long long idle_delta = (t2.idle + t2.iowait) - (t1.idle + t1.iowait);
|
|
long long total_delta = t2.total - t1.total;
|
|
if (total_delta == 0) return;
|
|
|
|
kvdict_t d; kv_clear(&d);
|
|
kv_set(&d, "plugin", "cpu_monitor");
|
|
kv_set_dbl(&d, "cpu_percent", round(100.0*(1.0-(double)idle_delta/total_delta)*10)/10.0);
|
|
kv_set_dbl(&d, "cpu_user", round(100.0*(t2.user - t1.user) / total_delta*10)/10.0);
|
|
kv_set_dbl(&d, "cpu_system", round(100.0*(t2.sys - t1.sys) / total_delta*10)/10.0);
|
|
kv_set_dbl(&d, "cpu_idle", round(100.0*(t2.idle - t1.idle) / total_delta*10)/10.0);
|
|
if (t2.iowait || t1.iowait)
|
|
kv_set_dbl(&d, "cpu_iowait", round(100.0*(t2.iowait - t1.iowait) / total_delta*10)/10.0);
|
|
double la[3];
|
|
if (getloadavg(la, 3) == 3) {
|
|
kv_set_dbl(&d, "load_1min", round(la[0]*100)/100.0);
|
|
kv_set_dbl(&d, "load_5min", round(la[1]*100)/100.0);
|
|
kv_set_dbl(&d, "load_15min", round(la[2]*100)/100.0);
|
|
}
|
|
read_cpu_extras(&d);
|
|
kv_set_dbl(&d, "_timestamp", now_ts());
|
|
conn_send(c, "PLG", &d);
|
|
LOGD("sent cpu_monitor");
|
|
}
|
|
|
|
/* ============================================================
|
|
* Plugin: memory_monitor
|
|
* Linux: /proc/meminfo
|
|
* FreeBSD: sysctl vm.stats.vm.*
|
|
* NetBSD: sysctl vm.uvmexp (struct uvmexp_sysctl)
|
|
* ============================================================ */
|
|
|
|
/* emit the common kvdict fields and send */
|
|
static void mem_send(conn_t *c,
|
|
long long tot, long long used, long long av, long long fr,
|
|
long long act, long long ina, long long cac, long long buf,
|
|
long long stot, long long sused) {
|
|
kvdict_t d; kv_clear(&d);
|
|
kv_set(&d, "plugin", "memory_monitor");
|
|
kv_set_ull(&d, "memory_total", (unsigned long long)tot);
|
|
kv_set_ull(&d, "memory_used", (unsigned long long)used);
|
|
kv_set_ull(&d, "memory_available", (unsigned long long)av);
|
|
kv_set_ull(&d, "memory_free", (unsigned long long)fr);
|
|
char pct[32];
|
|
snprintf(pct, sizeof(pct), "%.1f", tot ? 100.0*used/tot : 0.0);
|
|
kv_set(&d, "memory_percent", pct);
|
|
if (buf) kv_set_ull(&d, "memory_buffers", (unsigned long long)buf);
|
|
if (cac) kv_set_ull(&d, "memory_cached", (unsigned long long)cac);
|
|
if (act) kv_set_ull(&d, "memory_active", (unsigned long long)act);
|
|
if (ina) kv_set_ull(&d, "memory_inactive", (unsigned long long)ina);
|
|
if (stot > 0) {
|
|
long long sfr = stot - sused;
|
|
kv_set_ull(&d, "swap_total", (unsigned long long)stot);
|
|
kv_set_ull(&d, "swap_used", (unsigned long long)sused);
|
|
kv_set_ull(&d, "swap_free", (unsigned long long)sfr);
|
|
snprintf(pct, sizeof(pct), "%.1f", 100.0*sused/stot);
|
|
kv_set(&d, "swap_percent", pct);
|
|
}
|
|
kv_set_dbl(&d, "_timestamp", now_ts());
|
|
conn_send(c, "PLG", &d);
|
|
LOGD("sent memory_monitor");
|
|
}
|
|
|
|
#ifdef __linux__
|
|
|
|
static void plugin_memory_monitor(conn_t *c, const config_t *cfg) {
|
|
(void)cfg;
|
|
FILE *f = fopen("/proc/meminfo", "r");
|
|
if (!f) return;
|
|
long long tot=0, fr=0, av=0, buf=0, cac=0, act=0, ina=0, stot=0, sfr=0;
|
|
char line[256];
|
|
while (fgets(line, sizeof(line), f)) {
|
|
long long v; char key[64];
|
|
if (sscanf(line, "%63s %lld", key, &v) != 2) continue;
|
|
int kl = strlen(key); if (key[kl-1]==':') key[kl-1]='\0';
|
|
if (!strcmp(key,"MemTotal")) tot=v;
|
|
else if (!strcmp(key,"MemFree")) fr=v;
|
|
else if (!strcmp(key,"MemAvailable")) av=v;
|
|
else if (!strcmp(key,"Buffers")) buf=v;
|
|
else if (!strcmp(key,"Cached")) cac=v;
|
|
else if (!strcmp(key,"Active")) act=v;
|
|
else if (!strcmp(key,"Inactive")) ina=v;
|
|
else if (!strcmp(key,"SwapTotal")) stot=v;
|
|
else if (!strcmp(key,"SwapFree")) sfr=v;
|
|
}
|
|
fclose(f);
|
|
if (!tot) return;
|
|
if (!av) av = fr;
|
|
|
|
/* ZFS ARC is reclaimable memory not counted in MemAvailable */
|
|
long long arc_kb = 0;
|
|
FILE *arc = fopen("/proc/spl/kstat/zfs/arcstats", "r");
|
|
if (arc) {
|
|
char ak[64]; long long av2; int at;
|
|
while (fscanf(arc, "%63s %d %lld\n", ak, &at, &av2) == 3)
|
|
if (!strcmp(ak,"size")) { arc_kb = av2/1024; break; }
|
|
fclose(arc);
|
|
}
|
|
av = (av + arc_kb < tot) ? av + arc_kb : tot;
|
|
long long used = tot - av;
|
|
/* values from /proc/meminfo are in kB */
|
|
mem_send(c, tot*1024, used*1024, av*1024, fr*1024,
|
|
act*1024, ina*1024, cac*1024, buf*1024,
|
|
stot*1024, (stot-sfr)*1024);
|
|
}
|
|
|
|
#elif defined(__FreeBSD__) || defined(__DragonFly__)
|
|
|
|
static void plugin_memory_monitor(conn_t *c, const config_t *cfg) {
|
|
(void)cfg;
|
|
long ps = getpagesize();
|
|
|
|
unsigned long physmem = 0; size_t len = sizeof(physmem);
|
|
sysctlbyname("hw.physmem", &physmem, &len, NULL, 0);
|
|
if (!physmem) return;
|
|
|
|
unsigned int v_free=0, v_inactive=0, v_active=0, v_cache=0;
|
|
len = sizeof(v_free); sysctlbyname("vm.stats.vm.v_free_count", &v_free, &len, NULL, 0);
|
|
len = sizeof(v_inactive); sysctlbyname("vm.stats.vm.v_inactive_count", &v_inactive, &len, NULL, 0);
|
|
len = sizeof(v_active); sysctlbyname("vm.stats.vm.v_active_count", &v_active, &len, NULL, 0);
|
|
len = sizeof(v_cache); sysctlbyname("vm.stats.vm.v_cache_count", &v_cache, &len, NULL, 0);
|
|
|
|
long long tot = (long long)physmem;
|
|
long long fr = (long long)v_free * ps;
|
|
long long act = (long long)v_active * ps;
|
|
long long ina = (long long)v_inactive * ps;
|
|
long long cac = (long long)v_cache * ps;
|
|
long long av = fr + ina + cac; if (av > tot) av = tot;
|
|
long long used = tot - av;
|
|
mem_send(c, tot, used, av, fr, act, ina, cac, 0, 0, 0);
|
|
}
|
|
|
|
#elif defined(__NetBSD__)
|
|
|
|
static void plugin_memory_monitor(conn_t *c, const config_t *cfg) {
|
|
(void)cfg;
|
|
struct uvmexp_sysctl uvm;
|
|
size_t len = sizeof(uvm);
|
|
int mib[2] = {CTL_VM, VM_UVMEXP2};
|
|
if (sysctl(mib, 2, &uvm, &len, NULL, 0) != 0) return;
|
|
|
|
long long ps = uvm.pagesize;
|
|
long long tot = (long long)uvm.npages * ps;
|
|
long long fr = (long long)uvm.free * ps;
|
|
long long act = (long long)uvm.active * ps;
|
|
long long ina = (long long)uvm.inactive * ps;
|
|
long long av = fr + ina; if (av > tot) av = tot;
|
|
long long used = tot - av;
|
|
long long stot = (long long)uvm.swpages * ps;
|
|
long long sinuse = (long long)uvm.swpginuse * ps;
|
|
mem_send(c, tot, used, av, fr, act, ina, 0, 0, stot, sinuse);
|
|
}
|
|
|
|
#endif /* platform memory */
|
|
|
|
/* ============================================================
|
|
* Plugin: disk_monitor (df -P)
|
|
* ============================================================ */
|
|
|
|
static void plugin_disk_monitor(conn_t *c, const config_t *cfg) {
|
|
FILE *f = popen("df -P", "r");
|
|
if (!f) return;
|
|
|
|
char *json = malloc(MAX_VAL); if (!json) { pclose(f); return; }
|
|
int jlen = 0; bool first = true;
|
|
json[jlen++] = '{';
|
|
|
|
char line[512];
|
|
if (!fgets(line, sizeof(line), f)) { pclose(f); free(json); return; } /* skip header */
|
|
while (fgets(line, sizeof(line), f)) {
|
|
char fs[256], mount[256], pcts[16];
|
|
long long tk, uk, ak; int pct;
|
|
if (sscanf(line, "%255s %lld %lld %lld %15s %255s", fs, &tk, &uk, &ak, pcts, mount) != 6) continue;
|
|
pct = atoi(pcts);
|
|
if (cfg->disk_mount_count > 0) {
|
|
bool found = false;
|
|
for (int i = 0; i < cfg->disk_mount_count; i++)
|
|
if (!strcmp(cfg->disk_mounts[i], mount)) { found=true; break; }
|
|
if (!found) continue;
|
|
}
|
|
/* JSON-escape the mount point */
|
|
char esc[512]; int ei = 0;
|
|
for (int i = 0; mount[i] && ei < (int)sizeof(esc)-2; i++) {
|
|
if (mount[i]=='"'||mount[i]=='\\') esc[ei++]='\\';
|
|
esc[ei++] = mount[i];
|
|
}
|
|
esc[ei] = '\0';
|
|
char entry[512];
|
|
int el = snprintf(entry, sizeof(entry),
|
|
"%s\"%s\":{\"total\":%lld,\"used\":%lld,\"free\":%lld,\"percent\":%d}",
|
|
first ? "" : ",", esc, tk*1024, uk*1024, ak*1024, pct);
|
|
if (jlen + el < MAX_VAL - 2) { memcpy(json+jlen, entry, el); jlen+=el; first=false; }
|
|
}
|
|
pclose(f);
|
|
json[jlen++] = '}'; json[jlen] = '\0';
|
|
if (first) { free(json); return; }
|
|
|
|
kvdict_t d; kv_clear(&d);
|
|
kv_set(&d, "plugin", "disk_monitor");
|
|
char *jval = malloc(MAX_VAL + 1);
|
|
if (jval) { snprintf(jval, MAX_VAL, "@%s", json); kv_set(&d, "partitions", jval); free(jval); }
|
|
kv_set_dbl(&d, "_timestamp", now_ts());
|
|
conn_send(c, "PLG", &d);
|
|
free(json);
|
|
LOGD("sent disk_monitor");
|
|
}
|
|
|
|
/* ============================================================
|
|
* Plugin: network_monitor
|
|
* Linux: /proc/net/dev
|
|
* BSD: getifaddrs() + struct if_data (AF_LINK)
|
|
* ============================================================ */
|
|
|
|
typedef struct { char iface[64]; unsigned long long rx, tx; } net_stat_t;
|
|
static net_stat_t g_net_prev[64];
|
|
static int g_net_prev_n = 0;
|
|
static double g_net_prev_ts = 0;
|
|
|
|
#ifdef __linux__
|
|
|
|
static int read_net_stats(net_stat_t *out, int max) {
|
|
FILE *f = fopen("/proc/net/dev", "r");
|
|
if (!f) return 0;
|
|
char line[512]; int n = 0;
|
|
if (!fgets(line, sizeof(line), f)) { fclose(f); return 0; } /* header 1 */
|
|
if (!fgets(line, sizeof(line), f)) { fclose(f); return 0; } /* header 2 */
|
|
while (fgets(line, sizeof(line), f) && n < max) {
|
|
char *col = strchr(line, ':');
|
|
if (!col) continue;
|
|
*col = '\0';
|
|
char *iface = line; while (*iface==' ') iface++;
|
|
unsigned long long rx, tx, dummy;
|
|
if (sscanf(col+1, "%llu %llu %llu %llu %llu %llu %llu %llu %llu",
|
|
&rx,&dummy,&dummy,&dummy,&dummy,&dummy,&dummy,&dummy,&tx) == 9) {
|
|
snprintf(out[n].iface, 64, "%.*s", 63, iface);
|
|
out[n].rx = rx; out[n].tx = tx; n++;
|
|
}
|
|
}
|
|
fclose(f); return n;
|
|
}
|
|
|
|
#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
|
|
|
|
static int read_net_stats(net_stat_t *out, int max) {
|
|
struct ifaddrs *ifap;
|
|
if (getifaddrs(&ifap) != 0) return 0;
|
|
int n = 0;
|
|
for (struct ifaddrs *ifa = ifap; ifa && n < max; ifa = ifa->ifa_next) {
|
|
if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_LINK) continue;
|
|
struct if_data *ifd = (struct if_data *)ifa->ifa_data;
|
|
if (!ifd) continue;
|
|
snprintf(out[n].iface, 64, "%.*s", 63, ifa->ifa_name);
|
|
out[n].rx = (unsigned long long)ifd->ifi_ibytes;
|
|
out[n].tx = (unsigned long long)ifd->ifi_obytes;
|
|
n++;
|
|
}
|
|
freeifaddrs(ifap);
|
|
return n;
|
|
}
|
|
|
|
#endif /* platform network */
|
|
|
|
static void plugin_network_monitor(conn_t *c, const config_t *cfg) {
|
|
net_stat_t curr[64];
|
|
int cn = read_net_stats(curr, 64);
|
|
double now = now_ts();
|
|
|
|
if (g_net_prev_n == 0) {
|
|
memcpy(g_net_prev, curr, cn * sizeof(net_stat_t));
|
|
g_net_prev_n = cn; g_net_prev_ts = now;
|
|
return;
|
|
}
|
|
double dt = now - g_net_prev_ts;
|
|
if (dt <= 0) return;
|
|
|
|
char *json = malloc(MAX_VAL); if (!json) return;
|
|
int jlen = 0; bool first = true;
|
|
json[jlen++] = '{';
|
|
|
|
for (int i = 0; i < cn; i++) {
|
|
bool skip = false;
|
|
for (int s = 0; s < cfg->net_skip_count; s++)
|
|
if (!strcmp(cfg->net_skip[s], curr[i].iface)) { skip=true; break; }
|
|
if (skip) continue;
|
|
net_stat_t *prev = NULL;
|
|
for (int j = 0; j < g_net_prev_n; j++)
|
|
if (!strcmp(g_net_prev[j].iface, curr[i].iface)) { prev=&g_net_prev[j]; break; }
|
|
if (!prev) continue;
|
|
long long rd = (long long)(curr[i].rx - prev->rx);
|
|
long long td = (long long)(curr[i].tx - prev->tx);
|
|
char entry[256];
|
|
int el = snprintf(entry, sizeof(entry),
|
|
"%s\"%s\":{\"bytes_recv\":%llu,\"bytes_sent\":%llu"
|
|
",\"bytes_recv_delta\":%lld,\"bytes_sent_delta\":%lld}",
|
|
first?"":"," , curr[i].iface, curr[i].rx, curr[i].tx, rd, td);
|
|
if (jlen + el < MAX_VAL - 2) { memcpy(json+jlen, entry, el); jlen+=el; first=false; }
|
|
}
|
|
json[jlen++] = '}'; json[jlen] = '\0';
|
|
memcpy(g_net_prev, curr, cn * sizeof(net_stat_t));
|
|
g_net_prev_n = cn; g_net_prev_ts = now;
|
|
if (first) { free(json); return; }
|
|
|
|
kvdict_t d; kv_clear(&d);
|
|
kv_set(&d, "plugin", "network_monitor");
|
|
char *jval = malloc(MAX_VAL + 1);
|
|
if (jval) { snprintf(jval, MAX_VAL, "@%s", json); kv_set(&d, "interfaces", jval); free(jval); }
|
|
kv_set_dbl(&d, "_timestamp", now_ts());
|
|
conn_send(c, "PLG", &d);
|
|
free(json);
|
|
LOGD("sent network_monitor");
|
|
}
|
|
|
|
/* ============================================================
|
|
* Plugin: ping_monitor
|
|
* ============================================================ */
|
|
|
|
static bool do_ping(const char *host, int count, int timeout,
|
|
double *rmin, double *ravg, double *rmax, double *loss) {
|
|
*rmin = *ravg = *rmax = 1.0/0.0; /* inf */
|
|
*loss = 100.0;
|
|
char cmd[512];
|
|
snprintf(cmd, sizeof(cmd), "ping -c %d -W %d %s 2>/dev/null", count, timeout, host);
|
|
FILE *f = popen(cmd, "r");
|
|
if (!f) return false;
|
|
char line[512]; bool got = false;
|
|
while (fgets(line, sizeof(line), f)) {
|
|
char *p = strstr(line, "% packet loss");
|
|
if (p) {
|
|
while (p > line && (isdigit((unsigned char)*(p-1)) || *(p-1)=='.')) p--;
|
|
*loss = atof(p);
|
|
}
|
|
p = strstr(line, "min/avg/max");
|
|
if (p) {
|
|
p = strchr(p, '=');
|
|
if (p) { sscanf(p+1, " %lf/%lf/%lf", rmin, ravg, rmax); got=true; }
|
|
}
|
|
}
|
|
pclose(f); return got;
|
|
}
|
|
|
|
static void plugin_ping_monitor(conn_t *c, const config_t *cfg) {
|
|
if (!cfg->ping_host_count) return;
|
|
kvdict_t d; kv_clear(&d);
|
|
kv_set(&d, "plugin", "ping_monitor");
|
|
for (int i = 0; i < cfg->ping_host_count; i++) {
|
|
double rmin, ravg, rmax, loss;
|
|
bool ok = do_ping(cfg->ping_hosts[i], cfg->ping_count, cfg->ping_timeout,
|
|
&rmin, &ravg, &rmax, &loss);
|
|
char key[300];
|
|
snprintf(key, sizeof(key), "%s", cfg->ping_hosts[i]);
|
|
for (char *p = key; *p; p++) if (!isalnum((unsigned char)*p) && *p!='_') *p='_';
|
|
char kb[512];
|
|
if (ok) {
|
|
snprintf(kb,sizeof(kb),"%s_rtt_min",key); kv_set_dbl(&d,kb,rmin);
|
|
snprintf(kb,sizeof(kb),"%s_rtt_avg",key); kv_set_dbl(&d,kb,ravg);
|
|
snprintf(kb,sizeof(kb),"%s_rtt_max",key); kv_set_dbl(&d,kb,rmax);
|
|
snprintf(kb,sizeof(kb),"%s_loss",key); kv_set_dbl(&d,kb,loss);
|
|
} else {
|
|
snprintf(kb,sizeof(kb),"%s_rtt_min",key); kv_set(&d,kb,"inf");
|
|
snprintf(kb,sizeof(kb),"%s_rtt_avg",key); kv_set(&d,kb,"inf");
|
|
snprintf(kb,sizeof(kb),"%s_rtt_max",key); kv_set(&d,kb,"inf");
|
|
snprintf(kb,sizeof(kb),"%s_loss",key); kv_set_dbl(&d,kb,100.0);
|
|
}
|
|
}
|
|
kv_set_dbl(&d, "_timestamp", now_ts());
|
|
conn_send(c, "PLG", &d);
|
|
LOGD("sent ping_monitor");
|
|
}
|
|
|
|
/* ============================================================
|
|
* Plugin: nagios_runner
|
|
* ============================================================ */
|
|
|
|
static const char *nagios_status_str(int rc) {
|
|
switch (rc) { case 0:return"OK"; case 1:return"WARNING"; case 2:return"CRITICAL"; default:return"UNKNOWN"; }
|
|
}
|
|
|
|
static void parse_perfdata(const char *output, kvdict_t *d, const char *prefix) {
|
|
const char *pipe = strchr(output, '|');
|
|
if (!pipe) return;
|
|
const char *p = pipe + 1;
|
|
while (*p) {
|
|
while (*p==' ') p++;
|
|
if (!*p) break;
|
|
char label[128]; int li = 0;
|
|
if (*p == '\'') {
|
|
p++;
|
|
while (*p && *p != '\'') label[li++] = *p++;
|
|
if (*p == '\'') p++;
|
|
} else {
|
|
while (*p && *p != '=') label[li++] = *p++;
|
|
}
|
|
label[li] = '\0';
|
|
if (*p != '=') { while (*p && *p!=' ') p++; continue; }
|
|
p++;
|
|
char val[64]; int vi = 0;
|
|
while (*p && (isdigit((unsigned char)*p) || *p=='.' || *p=='-')) val[vi++] = *p++;
|
|
val[vi] = '\0';
|
|
char uom[16]; int ui = 0;
|
|
while (*p && *p!=';' && *p!=' ') uom[ui++] = *p++;
|
|
uom[ui] = '\0';
|
|
char key[256];
|
|
snprintf(key, sizeof(key), "%s_%s", prefix, label); kv_set(d, key, val);
|
|
if (uom[0]) { snprintf(key,sizeof(key),"%s_%s_uom",prefix,label); kv_set(d,key,uom); }
|
|
for (int n=0; n<4 && *p==';'; n++) { p++; while (*p && *p!=';' && *p!=' ') p++; }
|
|
while (*p==' ') p++;
|
|
}
|
|
}
|
|
|
|
static void plugin_nagios_runner(conn_t *c, const config_t *cfg) {
|
|
if (!cfg->nagios_count) return;
|
|
kvdict_t d; kv_clear(&d);
|
|
kv_set(&d, "plugin", "nagios_runner");
|
|
for (int i = 0; i < cfg->nagios_count; i++) {
|
|
const char *name = cfg->nagios[i].name;
|
|
char output[4096] = ""; int rc = 3;
|
|
FILE *f = popen(cfg->nagios[i].cmd, "r");
|
|
if (f) {
|
|
size_t _nr = fread(output, 1, sizeof(output)-1, f); (void)_nr;
|
|
int ret = pclose(f);
|
|
rc = WIFEXITED(ret) ? WEXITSTATUS(ret) : 3;
|
|
if (rc < 0 || rc > 3) rc = 3;
|
|
}
|
|
char *nl = strchr(output, '\n'); if (nl) *nl = '\0';
|
|
char msg[512]; char *pipe = strchr(output,'|');
|
|
if (pipe) snprintf(msg, sizeof(msg), "%.*s", (int)(pipe-output), output);
|
|
else snprintf(msg, sizeof(msg), "%s", output);
|
|
char key[128];
|
|
snprintf(key,sizeof(key),"%s_status",name); kv_set(&d,key,nagios_status_str(rc));
|
|
snprintf(key,sizeof(key),"%s_status_code",name); kv_set_int(&d,key,rc);
|
|
snprintf(key,sizeof(key),"%s_output",name); kv_set(&d,key,msg);
|
|
parse_perfdata(output, &d, name);
|
|
}
|
|
kv_set_dbl(&d, "_timestamp", now_ts());
|
|
conn_send(c, "PLG", &d);
|
|
LOGD("sent nagios_runner");
|
|
}
|
|
|
|
/* ============================================================
|
|
* Monitor thread: all periodic plugins in one thread
|
|
* ============================================================ */
|
|
|
|
typedef struct { conn_t *conn; const config_t *cfg; } thread_ctx_t;
|
|
|
|
static void *monitor_thread(void *arg) {
|
|
thread_ctx_t *ctx = arg;
|
|
conn_t *conn = ctx->conn;
|
|
const config_t *cfg = ctx->cfg;
|
|
|
|
time_t next_cpu = time(NULL);
|
|
time_t next_mem = time(NULL);
|
|
time_t next_disk = time(NULL);
|
|
time_t next_net = time(NULL);
|
|
time_t next_ping = time(NULL);
|
|
time_t next_nagios = time(NULL);
|
|
|
|
/* Prime network baseline */
|
|
plugin_network_monitor(conn, cfg);
|
|
|
|
while (g_running) {
|
|
time_t now = time(NULL);
|
|
if (now >= next_cpu) { plugin_cpu_monitor(conn, cfg); next_cpu = now + cfg->cpu_interval; }
|
|
if (now >= next_mem) { plugin_memory_monitor(conn, cfg); next_mem = now + cfg->mem_interval; }
|
|
if (now >= next_disk) { plugin_disk_monitor(conn, cfg); next_disk = now + cfg->disk_interval; }
|
|
if (now >= next_net) { plugin_network_monitor(conn, cfg); next_net = now + cfg->net_interval; }
|
|
if (cfg->ping_host_count && now >= next_ping) {
|
|
plugin_ping_monitor(conn, cfg); next_ping = now + cfg->ping_interval;
|
|
}
|
|
if (cfg->nagios_count && now >= next_nagios) {
|
|
plugin_nagios_runner(conn, cfg); next_nagios = now + cfg->nagios_interval;
|
|
}
|
|
sleep(1);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* ============================================================
|
|
* Daemonize
|
|
* ============================================================ */
|
|
|
|
static void daemonize(void) {
|
|
for (int n = 0; n < 2; n++) {
|
|
pid_t pid = fork();
|
|
if (pid < 0) { perror("fork"); exit(1); }
|
|
if (pid > 0) exit(0);
|
|
if (n == 0) { setsid(); if (chdir("/")) {}; umask(0); }
|
|
}
|
|
int fd;
|
|
fd = open("/dev/zero", O_RDONLY); if (fd>=0) { dup2(fd,0); close(fd); }
|
|
fd = open("/dev/null", O_WRONLY); if (fd>=0) { dup2(fd,1); close(fd); }
|
|
fd = open("/dev/null", O_WRONLY); if (fd>=0) { dup2(fd,2); close(fd); }
|
|
}
|
|
|
|
/* ============================================================
|
|
* CLI
|
|
* ============================================================ */
|
|
|
|
static void usage(const char *prog) {
|
|
fprintf(stderr,
|
|
"Usage: %s [options] host [host...]\n"
|
|
" -b Send boot message\n"
|
|
" -c FILE Config file (JSON)\n"
|
|
" -m MSG Send one-shot message\n"
|
|
" -n NAME Override hostname\n"
|
|
" -4 Use IPv4 only\n"
|
|
" -6 Use IPv6 only\n"
|
|
" -d Daemonize\n"
|
|
" -v Verbose (info)\n"
|
|
" -x Debug\n"
|
|
" -h Help\n", prog);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
bool do_boot = false;
|
|
bool do_daemon = false;
|
|
const char *cfgpath = NULL;
|
|
const char *message = NULL;
|
|
const char *nameov = NULL;
|
|
int af_filter = 0;
|
|
|
|
int opt;
|
|
while ((opt = getopt(argc, argv, "bc:m:n:dvxh46")) != -1) {
|
|
switch (opt) {
|
|
case 'b': do_boot = true; break;
|
|
case 'c': cfgpath = optarg; break;
|
|
case 'm': message = optarg; break;
|
|
case 'n': nameov = optarg; break;
|
|
case 'd': do_daemon = true; break;
|
|
case 'v': g_log_level = LL_INFO; break;
|
|
case 'x': g_log_level = LL_DEBUG; break;
|
|
case '4': af_filter = AF_INET; break;
|
|
case '6': af_filter = AF_INET6; break;
|
|
case 'h': usage(argv[0]); return 0;
|
|
default: usage(argv[0]); return 1;
|
|
}
|
|
}
|
|
if (optind >= argc) { fprintf(stderr,"error: host required\n"); usage(argv[0]); return 1; }
|
|
|
|
const char *hosts[MAX_HOSTS]; int nhost = 0;
|
|
for (int i = optind; i < argc && nhost < MAX_HOSTS; i++) hosts[nhost++] = argv[i];
|
|
|
|
config_t cfg;
|
|
config_load(&cfg, cfgpath);
|
|
|
|
if (do_daemon) {
|
|
daemonize();
|
|
g_use_syslog = true;
|
|
openlog("hbc", LOG_PID, LOG_DAEMON);
|
|
}
|
|
|
|
char iam[256];
|
|
if (nameov) {
|
|
snprintf(iam, sizeof(iam), "%s", nameov);
|
|
} else {
|
|
gethostname(iam, sizeof(iam));
|
|
char *dot = strchr(iam, '.'); if (dot) *dot = '\0';
|
|
}
|
|
|
|
struct sigaction sa = {0};
|
|
sa.sa_handler = sig_handler;
|
|
sigaction(SIGTERM, &sa, NULL);
|
|
sigaction(SIGINT, &sa, NULL);
|
|
sigaction(SIGHUP, &sa, NULL);
|
|
|
|
int conn_id = 1;
|
|
int retry_delay = 5;
|
|
while (g_running && !g_nconns) {
|
|
for (int i = 0; i < nhost; i++) {
|
|
struct addrinfo hints = {0}, *res = NULL;
|
|
hints.ai_socktype = SOCK_DGRAM;
|
|
hints.ai_protocol = IPPROTO_UDP;
|
|
hints.ai_family = af_filter;
|
|
char ps[16]; snprintf(ps, sizeof(ps), "%d", cfg.hb_port);
|
|
if (getaddrinfo(hosts[i], ps, &hints, &res) != 0) {
|
|
LOGW("cannot resolve %s — retrying in %ds", hosts[i], retry_delay);
|
|
continue;
|
|
}
|
|
for (struct addrinfo *ai = res; ai && g_nconns < MAX_HOSTS; ai = ai->ai_next) {
|
|
conn_t *c = &g_conns[g_nconns];
|
|
memset(c, 0, sizeof(*c));
|
|
c->conn_id = conn_id++; c->port = cfg.hb_port;
|
|
c->af = ai->ai_family; c->sockfd = -1;
|
|
snprintf(c->name, sizeof(c->name), "%s", iam);
|
|
void *addr = (ai->ai_family == AF_INET)
|
|
? (void *)&((struct sockaddr_in *)ai->ai_addr)->sin_addr
|
|
: (void *)&((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr;
|
|
inet_ntop(ai->ai_family, addr, c->addr, sizeof(c->addr));
|
|
if (conn_open(c)) { g_nconns++; LOGI("connected to %s", c->addr); }
|
|
}
|
|
freeaddrinfo(res);
|
|
}
|
|
if (!g_nconns) {
|
|
sleep(retry_delay);
|
|
if (retry_delay < 60) retry_delay *= 2;
|
|
}
|
|
}
|
|
if (!g_nconns) return 1;
|
|
|
|
conn_t *primary = &g_conns[0];
|
|
LOGI("hbc_mini-c %s on %s -> %s port=%d interval=%ds",
|
|
HBC_VERSION, iam, hosts[0], cfg.hb_port, cfg.interval);
|
|
|
|
/* Boot / one-shot message */
|
|
bool send_shutdown = false;
|
|
if (do_boot || message) {
|
|
kvdict_t bm; kv_clear(&bm);
|
|
kv_set_int(&bm, "acks", 0);
|
|
if (do_boot) { kv_set_int(&bm,"boot",1); send_shutdown=true; }
|
|
if (message) { kv_set(&bm,"service","service"); kv_set(&bm,"msg",message); }
|
|
conn_send(primary, "HTB", &bm);
|
|
if (message && !do_daemon) {
|
|
usleep(300000);
|
|
for (int i=0;i<g_nconns;i++) conn_close(&g_conns[i]);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* Info plugins at startup */
|
|
plugin_os_info(primary, &cfg);
|
|
|
|
/* Monitor thread */
|
|
thread_ctx_t tctx = {primary, &cfg};
|
|
pthread_t tid;
|
|
pthread_create(&tid, NULL, monitor_thread, &tctx);
|
|
|
|
/* Main heartbeat + receive loop */
|
|
time_t last_hb = 0;
|
|
while (g_running) {
|
|
time_t now = time(NULL);
|
|
if (now - last_hb >= cfg.interval) {
|
|
for (int i = 0; i < g_nconns; i++) {
|
|
kvdict_t hb; kv_clear(&hb);
|
|
kv_set_int(&hb, "acks", g_conns[i].ack_count);
|
|
kv_set_dbl(&hb, "rtt", g_conns[i].rtt);
|
|
kv_set_int(&hb, "interval", cfg.interval);
|
|
conn_send(&g_conns[i], "HTB", &hb);
|
|
}
|
|
last_hb = now;
|
|
}
|
|
|
|
fd_set rset; FD_ZERO(&rset); int maxfd = -1;
|
|
for (int i = 0; i < g_nconns; i++) {
|
|
if (g_conns[i].sockfd >= 0) { FD_SET(g_conns[i].sockfd,&rset); if(g_conns[i].sockfd>maxfd) maxfd=g_conns[i].sockfd; }
|
|
}
|
|
if (maxfd >= 0) {
|
|
struct timeval tv = {1, 0};
|
|
if (select(maxfd+1, &rset, NULL, NULL, &tv) > 0)
|
|
for (int i = 0; i < g_nconns; i++)
|
|
if (g_conns[i].sockfd>=0 && FD_ISSET(g_conns[i].sockfd,&rset))
|
|
conn_recv(&g_conns[i]);
|
|
} else { sleep(1); }
|
|
|
|
if (primary->request_info) {
|
|
primary->request_info = false;
|
|
LOGI("refreshing info plugins on server request");
|
|
plugin_os_info(primary, &cfg);
|
|
}
|
|
}
|
|
|
|
pthread_cancel(tid);
|
|
pthread_join(tid, NULL);
|
|
|
|
if (send_shutdown) {
|
|
kvdict_t sh; kv_clear(&sh);
|
|
kv_set_int(&sh, "shutdown", 1);
|
|
kv_set_int(&sh, "acks", primary->ack_count);
|
|
conn_send(primary, "HTB", &sh);
|
|
usleep(300000);
|
|
}
|
|
for (int i = 0; i < g_nconns; i++) conn_close(&g_conns[i]);
|
|
if (g_use_syslog) closelog();
|
|
if (g_restart) { LOGI("restarting..."); execv(argv[0], argv); }
|
|
return 0;
|
|
}
|