diff options
-rw-r--r-- | collector.h | 15 | ||||
-rw-r--r-- | cpu.c | 129 | ||||
-rw-r--r-- | cpu.h | 8 | ||||
-rw-r--r-- | hwmon.c | 200 | ||||
-rw-r--r-- | hwmon.h | 8 | ||||
-rw-r--r-- | main.c | 139 | ||||
-rw-r--r-- | network.c | 186 | ||||
-rw-r--r-- | network.h | 8 | ||||
-rw-r--r-- | scrape.c | 152 | ||||
-rw-r--r-- | scrape.h | 35 | ||||
-rw-r--r-- | textfile.c | 59 | ||||
-rw-r--r-- | textfile.h | 11 | ||||
-rw-r--r-- | uname.c | 55 | ||||
-rw-r--r-- | uname.h | 8 | ||||
-rw-r--r-- | util.c | 197 | ||||
-rw-r--r-- | util.h | 62 |
16 files changed, 1272 insertions, 0 deletions
diff --git a/collector.h b/collector.h new file mode 100644 index 0000000..8ab9c61 --- /dev/null +++ b/collector.h @@ -0,0 +1,15 @@ +#ifndef PNANOE_COLLECTOR_H_ +#define PNANOE_COLLECTOR_H_ 1 + +#include "stdbool.h" + +#include "scrape.h" + +struct collector { + const char *name; + void (*collect)(scrape_req *req, void *ctx); + void *(*init)(int argc, char *argv[]); + bool has_args; +}; + +#endif // PNANOE_COLLECTOR_H_ diff --git a/cpu.c b/cpu.c new file mode 100644 index 0000000..f73e235 --- /dev/null +++ b/cpu.c @@ -0,0 +1,129 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "cpu.h" +#include "util.h" + +// limits for CPU numbers +#define MAX_CPU_ID 9999999 +#define MAX_CPU_DIGITS 7 +// size of input buffer for reading lines +#define BUF_SIZE 256 + +static void *cpu_init(int argc, char *argv[]); +static void cpu_collect(scrape_req *req, void *ctx_ptr); + +const struct collector cpu_collector = { + .name = "cpu", + .collect = cpu_collect, + .init = cpu_init, +}; + +struct cpu_context { + long clock_tick; +}; + +void *cpu_init(int argc, char *argv[]) { + long clock_tick = sysconf(_SC_CLK_TCK); + if (clock_tick <= 0) { + perror("sysconf(_SC_CLK_TCK)"); + return 0; + } + + struct cpu_context *ctx = malloc(sizeof *ctx); + if (!ctx) + return 0; + ctx->clock_tick = clock_tick; + return ctx; +} + +void cpu_collect(scrape_req *req, void *ctx_ptr) { + struct cpu_context *ctx = ctx_ptr; + + // buffers + + char cpu_label[MAX_CPU_DIGITS + 1] = ""; + static const char *modes[] = { + "user", "nice", "system", "idle", "iowait", "irq", "softirq", "steal", + 0, + }; + + const char *stat_labels[][2] = { + { "cpu", cpu_label }, + { "mode", 0 }, // filled by code + { 0, 0 }, + }; + const char *freq_labels[][2] = { + { "cpu", cpu_label }, + { 0, 0 }, + }; + + char buf[BUF_SIZE]; + + FILE *f; + + // collect node_cpu_seconds_total metrics from /proc/stat + + f = fopen("/proc/stat", "r"); + if (f) { + while (fgets_line(buf, sizeof buf, f)) { + if (strncmp(buf, "cpu", 3) != 0 || (buf[3] < '0' || buf[3] > '9')) + continue; + + char *at = buf + 3; + char *sep = strchr(at, ' '); + if (!sep || sep - at + 1 > sizeof cpu_label) + continue; + *sep = '\0'; + strcpy(cpu_label, at); + + at = sep + 1; + for (const char **mode = modes; *mode; mode++) { + while (*at == ' ') + at++; + sep = strpbrk(at, " \n"); + if (!sep) + break; + *sep = '\0'; + + char *endptr; + double value = strtod(at, &endptr); + if (*endptr != '\0') + break; + value /= ctx->clock_tick; + + stat_labels[1][1] = *mode; + scrape_write(req, "node_cpu_seconds_total", stat_labels, value); + + at = sep + 1; + } + } + fclose(f); + } + + // collect node_cpu_frequency_hertz metrics from /sys/devices/system/cpu/cpu*/cpufreq + + for (int cpu = 0; cpu <= MAX_CPU_ID; cpu++) { +#define PATH_FORMAT "/sys/devices/system/cpu/cpu%d/cpufreq/scaling_cur_freq" + char path[sizeof PATH_FORMAT - 2 + MAX_CPU_DIGITS + 1]; + snprintf(path, sizeof path, PATH_FORMAT, cpu); + + f = fopen(path, "r"); + if (!f) + break; + + if (fgets(buf, sizeof buf, f)) { + char *endptr; + double value = strtod(buf, &endptr); + if (*endptr == '\0' || *endptr == '\n') { + value *= 1000; + snprintf(cpu_label, sizeof cpu_label, "%d", cpu); + scrape_write(req, "node_cpu_frequency_hertz", freq_labels, value); + } + } + + fclose(f); + } +} diff --git a/cpu.h b/cpu.h new file mode 100644 index 0000000..3bc0d1f --- /dev/null +++ b/cpu.h @@ -0,0 +1,8 @@ +#ifndef PNANOE_CPU_H_ +#define PNANOE_CPU_H_ 1 + +#include "collector.h" + +extern const struct collector cpu_collector; + +#endif // PNANOE_CPU_H_ diff --git a/hwmon.c b/hwmon.c new file mode 100644 index 0000000..70c3129 --- /dev/null +++ b/hwmon.c @@ -0,0 +1,200 @@ +#define _POSIX_C_SOURCE 200809L + +#include <dirent.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "hwmon.h" + +// size of input buffer for paths and lines +#define BUF_SIZE 256 +// size of input buffer for labels +#define LABEL_SIZE 32 + +static void hwmon_collect(scrape_req *req, void *ctx); + +const struct collector hwmon_collector = { + .name = "hwmon", + .collect = hwmon_collect, +}; + +static double hwmon_conv_millis(const char *text) { + char *endptr; + double value = strtod(text, &endptr); + if (*endptr == '\n' || *endptr == '\0') + return value / 1000; + return NAN; +} + +static double hwmon_conv_id(const char *text) { + char *endptr; + double value = strtod(text, &endptr); + if (*endptr == '\n' || *endptr == '\0') + return value; + return NAN; +} + +static double hwmon_conv_flag(const char *text) { + if ((text[0] == '0' || text[0] == '1') && (text[1] == '\n' || text[1] == '\0')) + return text[0] - '0'; + return NAN; +} + +struct metric_type { + const char *suffix; + const char *metric; + double (*conv)(const char *text); +}; + +struct metric_data { + const char *prefix; + const struct metric_type *types; +}; + +static const struct metric_data metrics[] = { + { + .prefix = "in", + .types = (const struct metric_type[]){ + { + .suffix = "_input", + .metric = "node_hwmon_in_volts", + .conv = hwmon_conv_millis, + }, + { + .suffix = "_min", + .metric = "node_hwmon_in_min_volts", + .conv = hwmon_conv_millis, + }, + { + .suffix = "_max", + .metric = "node_hwmon_in_max_volts", + .conv = hwmon_conv_millis, + }, + { + .suffix = "_alarm", + .metric = "node_hwmon_in_alarm", + .conv = hwmon_conv_flag, + }, + { .suffix = 0 }, + }, + }, + { + .prefix = "fan", + .types = (const struct metric_type[]){ + { + .suffix = "_input", + .metric = "node_hwmon_fan_rpm", + .conv = hwmon_conv_id, + }, + { + .suffix = "_min", + .metric = "node_hwmon_fan_min_rpm", + .conv = hwmon_conv_id, + }, + { + .suffix = "_alarm", + .metric = "node_hwmon_fan_alarm", + .conv = hwmon_conv_flag, + }, + { .suffix = 0 }, + }, + }, + { + .prefix = "temp", + .types = (const struct metric_type[]){ + { + .suffix = "_input", + .metric = "node_hwmon_temp_celsius", + .conv = hwmon_conv_millis, + }, + { .suffix = 0 }, + }, + }, + { .prefix = 0 }, +}; + +static void hwmon_collect(scrape_req *req, void *ctx) { + // buffers + + char chip_label[LABEL_SIZE]; + char sensor_label[LABEL_SIZE]; + + const char *labels[][2] = { + { "chip", chip_label }, + { "sensor", sensor_label }, + { 0, 0 }, + }; + + char path[BUF_SIZE]; + char buf[BUF_SIZE]; + size_t len; + + FILE *f; + + DIR *root; + struct dirent *dent; + + // iterate over all hwmon instances in /sys/class/hwmon + + root = opendir("/sys/class/hwmon"); + if (!root) + return; + + while ((dent = readdir(root))) { + if (strncmp(dent->d_name, "hwmon", 5) != 0) + continue; + snprintf(path, sizeof path, "/sys/class/hwmon/%s", dent->d_name); + + len = readlink(path, buf, sizeof buf); + if (len > 14 && memcmp(buf, "../../devices/", 14) == 0) { + char *start = buf + 14; + char *end = strchr(start, '/'); + if (end) + end = strchr(end + 1, '/'); + if (end) + *end = '\0'; + snprintf(chip_label, sizeof chip_label, "%s", start); + } else { + snprintf(chip_label, sizeof chip_label, "unknown"); + } + + DIR *dir = opendir(path); + if (!dir) + continue; + + while ((dent = readdir(dir))) { + for (const struct metric_data *metric = metrics; metric->prefix; metric++) { + if (strncmp(dent->d_name, metric->prefix, strlen(metric->prefix)) != 0) + continue; + char *suffix = strchr(dent->d_name, '_'); + if (!suffix) + continue; + + snprintf(sensor_label, sizeof sensor_label, "%.*s", (int)(suffix - dent->d_name), dent->d_name); + + for (const struct metric_type *type = metric->types; type->suffix; type++) { + if (strcmp(suffix, type->suffix) != 0) + continue; + + snprintf(buf, sizeof buf, "%s/%s", path, dent->d_name); + f = fopen(buf, "r"); + if (!f) + continue; + if (fgets(buf, sizeof buf, f)) { + double value = type->conv(buf); + if (!isnan(value)) + scrape_write(req, type->metric, labels, value); + } + fclose(f); + } + } + } + + closedir(dir); + } + + closedir(root); +} diff --git a/hwmon.h b/hwmon.h new file mode 100644 index 0000000..14315a1 --- /dev/null +++ b/hwmon.h @@ -0,0 +1,8 @@ +#ifndef PNANOE_HWMON_H_ +#define PNANOE_HWMON_H_ 1 + +#include "collector.h" + +extern const struct collector hwmon_collector; + +#endif // PNANOE_HWMON_H_ diff --git a/main.c b/main.c new file mode 100644 index 0000000..09e2842 --- /dev/null +++ b/main.c @@ -0,0 +1,139 @@ +#define _POSIX_C_SOURCE 200809L + +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> +#include <string.h> + +#include "cpu.h" +#include "hwmon.h" +#include "network.h" +#include "scrape.h" +#include "textfile.h" +#include "uname.h" +#include "util.h" + +static const struct collector *collectors[] = { + &cpu_collector, + &hwmon_collector, + &network_collector, + &textfile_collector, + &uname_collector, +}; +#define NCOLLECTORS (sizeof collectors / sizeof *collectors) + +enum tristate { flag_off = -1, flag_undef = 0, flag_on = 1 }; + +struct config { + const char *port; +}; + +struct handler_ctx { + struct { + enum tristate enabled; + struct slist *args; + struct slist **next_arg; + void *ctx; + } collectors[NCOLLECTORS]; +}; + +static bool parse_args(int argc, char *argv[], struct config *cfg, struct handler_ctx *ctx); +static bool initialize(struct handler_ctx *ctx, int max_args); +static void handler(scrape_req *req, void *ctx_ptr); + +int main(int argc, char *argv[]) { + struct config cfg = { + .port = "29100", + }; + struct handler_ctx ctx; + + if (!parse_args(argc, argv, &cfg, &ctx)) + return 1; + if (!initialize(&ctx, argc > 0 ? argc - 1 : 0)) + return 1; + if (!scrape_serve(cfg.port, handler, &ctx)) + return 1; + + return 0; +}; + +static bool parse_args(int argc, char *argv[], struct config *cfg, struct handler_ctx *ctx) { + for (size_t i = 0; i < NCOLLECTORS; i++) { + ctx->collectors[i].args = 0; + ctx->collectors[i].next_arg = &ctx->collectors[i].args; + ctx->collectors[i].enabled = flag_undef; + } + + enum tristate enabled_default = flag_on; + + for (int arg = 1; arg < argc; arg++) { + // check for collector arguments + + for (size_t i = 0; i < NCOLLECTORS; i++) { + size_t name_len = strlen(collectors[i]->name); + if (strncmp(argv[arg], "--", 2) == 0 + && strncmp(argv[arg] + 2, collectors[i]->name, name_len) == 0 + && argv[arg][2 + name_len] == '-') { + char *carg = argv[arg] + 2 + name_len + 1; + if (strcmp(carg, "on") == 0) { + ctx->collectors[i].enabled = flag_on; + enabled_default = flag_off; + } else if (strcmp(carg, "off") == 0) { + ctx->collectors[i].enabled = flag_off; + } else if (collectors[i]->init && collectors[i]->has_args) { + ctx->collectors[i].next_arg = slist_append(ctx->collectors[i].next_arg, carg); + } else { + fprintf(stderr, "unknown argument: %s (collector %s takes no arguments)\n", argv[arg], collectors[i]->name); + return false; + } + goto next_arg; + } + } + + // parse any non-collector arguments + + // TODO --help, --port=X + + fprintf(stderr, "unknown argument: %s\n", argv[arg]); + return false; + next_arg: ; + } + + for (size_t i = 0; i < NCOLLECTORS; i++) + if (ctx->collectors[i].enabled == flag_undef) + ctx->collectors[i].enabled = enabled_default; + + return true; +} + +static bool initialize(struct handler_ctx *ctx, int max_args) { + int argc; + char *argv[max_args + 1]; + + for (size_t i = 0; i < NCOLLECTORS; i++) { + if (ctx->collectors[i].enabled == flag_on && collectors[i]->init) { + argc = 0; + for (struct slist *arg = ctx->collectors[i].args; arg && argc < max_args; arg = arg->next) + argv[argc++] = arg->data; + argv[argc] = 0; + + ctx->collectors[i].ctx = collectors[i]->init(argc, argv); + + if (!ctx->collectors[i].ctx) { + fprintf(stderr, "failed to initialize collector %s\n", collectors[i]->name); + return false; + } + } + } + + return true; +} + +static void handler(scrape_req *req, void *ctx_ptr) { + struct handler_ctx *ctx = ctx_ptr; + + for (size_t c = 0; c < NCOLLECTORS; c++) { + if (ctx->collectors[c].enabled == flag_on) + collectors[c]->collect(req, ctx->collectors[c].ctx); + } +} diff --git a/network.c b/network.c new file mode 100644 index 0000000..86f3b43 --- /dev/null +++ b/network.c @@ -0,0 +1,186 @@ +#define _POSIX_C_SOURCE 200809L + +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "network.h" +#include "util.h" + +// size of input buffer for paths and lines +#define BUF_SIZE 256 +// maximum number of columns in the file +#define MAX_COLUMNS 32 + +// default list of interfaces to exclude +#define DEFAULT_EXCLUDE "lo" + +static void *network_init(int argc, char *argv[]); +static void network_collect(scrape_req *req, void *ctx); + +const struct collector network_collector = { + .name = "network", + .collect = network_collect, + .init = network_init, + .has_args = true, +}; + +struct network_context { + size_t ncolumns; + char *columns[MAX_COLUMNS]; + struct slist *include; + struct slist *exclude; +}; + +static void *network_init(int argc, char *argv[]) { + // parse header from /proc/net/dev and prepare metric names + + FILE *f = fopen("/proc/net/dev", "r"); + if (!f) { + perror("fopen /proc/net/dev"); + return 0; + } + + char buf[BUF_SIZE]; + char *p; + + fgets_line(buf, sizeof buf, f); + p = fgets_line(buf, sizeof buf, f); + fclose(f); + if (!p) { + fprintf(stderr, "second header line in /proc/net/dev missing\n"); + return 0; + } + + static const char *const prefixes[2] = { "node_network_receive_", "node_network_transmit_" }; + char *parts[2]; + char *saveptr; + + strtok_r(buf, "|", &saveptr); + parts[0] = strtok_r(0, "|", &saveptr); + parts[1] = strtok_r(0, "|", &saveptr); + p = strtok_r(0, "|", &saveptr); + if (!parts[0] || !parts[1] || p) { + fprintf(stderr, "too %s parts in /proc/net/dev header\n", p ? "many" : "few"); + return 0; + } + + struct network_context *ctx = malloc(sizeof *ctx); + if (!ctx) { + perror("malloc"); + return 0; + } + + ctx->ncolumns = 0; + for (int part = 0; part < 2; part++) { + size_t prefix_len = strlen(prefixes[part]); + + for (p = strtok_r(parts[part], " \n", &saveptr); p; p = strtok_r(0, " \n", &saveptr)) { + if (ctx->ncolumns >= MAX_COLUMNS) { + fprintf(stderr, "too many columns in /proc/net/dev\n"); + goto cleanup; + } + + size_t header_len = strlen(p); + size_t metric_len = prefix_len + header_len + 6; // 6 for "_total" + + ctx->columns[ctx->ncolumns] = malloc(metric_len + 1); + if (!ctx->columns[ctx->ncolumns]) { + perror("malloc"); + goto cleanup; + } + + snprintf(ctx->columns[ctx->ncolumns], metric_len + 1, "%s%s_total", prefixes[part], p); + ctx->ncolumns++; + } + } + + // parse command-line arguments + + ctx->include = 0; + ctx->exclude = 0; + bool exclude_set = false; + + for (int arg = 0; arg < argc; arg++) { + if (strncmp(argv[arg], "include=", 8) == 0) { + ctx->include = slist_split(argv[arg] + 8, ","); + continue; + } + if (strncmp(argv[arg], "exclude=", 8) == 0) { + ctx->exclude = slist_split(argv[arg] + 8, ","); + continue; + } + + fprintf(stderr, "unknown argument for network collector: %s\n", argv[arg]); + goto cleanup; + } + + if (!exclude_set) + ctx->exclude = slist_split(DEFAULT_EXCLUDE, ","); + + return ctx; + +cleanup: + for (size_t i = 0; i < ctx->ncolumns; i++) + free(ctx->columns[i]); + free(ctx); + return 0; +} + +static void network_collect(scrape_req *req, void *ctx_ptr) { + struct network_context *ctx = ctx_ptr; + + // buffers + + const char *labels[][2] = { + { "device", 0 }, // filled by code + { 0, 0 }, + }; + + char buf[BUF_SIZE]; + + FILE *f; + + // read network stats from /proc/net/dev + + f = fopen("/proc/net/dev", "r"); + if (!f) + return; + + fgets_line(buf, sizeof buf, f); + fgets_line(buf, sizeof buf, f); // skipped header + + while (fgets_line(buf, sizeof buf, f)) { + char *dev = buf; + while (*dev == ' ') + dev++; + + char *p = strchr(dev, ':'); + if (!p) + continue; + *p = '\0'; + labels[0][1] = dev; + p++; + + if (ctx->include) { + if (!slist_contains(ctx->include, dev)) + continue; + } else if (ctx->exclude) { + if (slist_contains(ctx->exclude, dev)) + continue; + } + + char *saveptr; + p = strtok_r(p, " \n", &saveptr); + for (size_t i = 0; i < ctx->ncolumns && p; i++, p = strtok_r(0, " \n", &saveptr)) { + char *endptr; + double value = strtod(p, &endptr); + if (*endptr != '\0') + continue; + scrape_write(req, ctx->columns[i], labels, value); + } + } + + fclose(f); +} diff --git a/network.h b/network.h new file mode 100644 index 0000000..98d7241 --- /dev/null +++ b/network.h @@ -0,0 +1,8 @@ +#ifndef PNANOE_NETWORK_H_ +#define PNANOE_NETWORK_H_ 1 + +#include "collector.h" + +extern const struct collector network_collector; + +#endif // PNANOE_NETWORK_H_ diff --git a/scrape.c b/scrape.c new file mode 100644 index 0000000..33e22fc --- /dev/null +++ b/scrape.c @@ -0,0 +1,152 @@ +#define _POSIX_C_SOURCE 200809L + +#include <netdb.h> +#include <netinet/in.h> +#include <poll.h> +#include <stdio.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <unistd.h> + +#include "scrape.h" +#include "util.h" + +#define BUF_INITIAL 1024 +#define BUF_MAX 65536 + +#define MAX_LISTEN_SOCKETS 4 +#define MAX_BACKLOG 16 + +struct scrape_req { + int socket; + cbuf *buf; +}; + +bool scrape_serve(const char *port, scrape_handler *handler, void *handler_ctx) { + struct scrape_req req; + + struct pollfd fds[MAX_LISTEN_SOCKETS]; + nfds_t nfds = 0; + + int ret; + + { + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + .ai_protocol = 0, + .ai_flags = AI_PASSIVE | AI_ADDRCONFIG, + }; + struct addrinfo *addrs; + + ret = getaddrinfo(0, port, &hints, &addrs); + if (ret != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret)); + return false; + } + + for (struct addrinfo *a = addrs; a && nfds < MAX_LISTEN_SOCKETS; a = a->ai_next) { + int s = socket(a->ai_family, a->ai_socktype, a->ai_protocol); + if (s == -1) { + perror("socket"); + continue; + } + + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, sizeof (int)); +#if defined(IPPROTO_IPV6) && defined(IPV6_V6ONLY) && defined(AF_INET6) + if (a->ai_family == AF_INET6) + setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (int[]){1}, sizeof (int)); +#endif + + ret = bind(s, a->ai_addr, a->ai_addrlen); + if (ret == -1) { + perror("bind"); + close(s); + continue; + } + + ret = listen(s, MAX_BACKLOG); + if (ret == -1) { + perror("listen"); + close(s); + continue; + } + + fds[nfds].fd = s; + fds[nfds].events = POLLIN; + nfds++; + } + } + + if (nfds == 0) { + fprintf(stderr, "failed to bind any sockets"); + return false; + } + + req.buf = cbuf_alloc(BUF_INITIAL, BUF_MAX); + if (!req.buf) { + perror("cbuf_alloc"); + for (nfds_t i = 0; i < nfds; i++) + close(fds[i].fd); + return false; + } + + while (1) { + ret = poll(fds, nfds, -1); + if (ret == -1) { + perror("poll"); + break; + } + + for (nfds_t i = 0; i < nfds; i++) { + if (fds[i].revents == 0) + continue; + if (fds[i].revents != POLLIN) { + fprintf(stderr, "poll .revents = %d\n", fds[i].revents); + goto break_loop; + } + + req.socket = accept(fds[i].fd, 0, 0); + if (req.socket == -1) { + perror("accept"); + continue; + } + + // TODO: HTTP stuff + handler(&req, handler_ctx); + close(req.socket); + } + } +break_loop: + + for (nfds_t i = 0; i < nfds; i++) + close(fds[i].fd); + + return false; +} + +void scrape_write(scrape_req *req, const char *metric, const char *(*labels)[2], double value) { + cbuf_reset(req->buf); + + cbuf_puts(req->buf, metric); + + if (labels && (*labels)[0]) { + cbuf_putc(req->buf, '{'); + for (const char *(*l)[2] = labels; (*l)[0]; l++) { + if (l != labels) + cbuf_putc(req->buf, ','); + cbuf_putf(req->buf, "%s=\"%s\"", (*l)[0], (*l)[1]); + } + cbuf_putc(req->buf, '}'); + } + + cbuf_putf(req->buf, " %.16g\n", value); + + size_t buf_len; + const char *buf = cbuf_get(req->buf, &buf_len); + write_all(req->socket, buf, buf_len); +} + +void scrape_write_raw(scrape_req *req, const void *buf, size_t len) { + write_all(req->socket, buf, len); +} diff --git a/scrape.h b/scrape.h new file mode 100644 index 0000000..fc4e18b --- /dev/null +++ b/scrape.h @@ -0,0 +1,35 @@ +#ifndef PNANOE_SCRAPE_H_ +#define PNANOE_SCRAPE_H_ 1 + +#include <stdbool.h> +#include <stddef.h> + +/** Opaque type to represent an ongoing scrape request. */ +typedef struct scrape_req scrape_req; + +/** Function type for a scrape server callback. */ +typedef void scrape_handler(scrape_req *req, void *ctx); + +/** Starts a scrape server in the given port. */ +bool scrape_serve(const char *port, scrape_handler *handler, void *handler_ctx); + +/** + * Writes a metric value as a response to a scrape. + * + * The \p labels parameter can be `NULL` if no extra labels need to be attached. If not null, it + * should point at the first element of an array of 2-element arrays of pointers, where the first + * element of each pair is the label name, and the second the label value. A pair of null pointers + * terminates the label array. + * + * Returns `false` if setting up the server failed, otherwise does not return. + */ +void scrape_write(scrape_req *req, const char *metric, const char *(*labels)[2], double value); + +/** + * Writes raw data to the scrape response. + * + * It's the callers responsibility to make sure it writes syntactically valid metric data. + */ +void scrape_write_raw(scrape_req *req, const void *buf, size_t len); + +#endif // PNANOE_SCRAPE_H_ diff --git a/textfile.c b/textfile.c new file mode 100644 index 0000000..b571f27 --- /dev/null +++ b/textfile.c @@ -0,0 +1,59 @@ +#include <stdbool.h> +#include <dirent.h> +#include <stdio.h> +#include <string.h> +#include <sys/types.h> + +#include "textfile.h" +#include "util.h" + +void *textfile_init(int argc, char *argv[]); +void textfile_collect(scrape_req *req, void *ctx); + +const struct collector textfile_collector = { + .name = "textfile", + .collect = textfile_collect, + .init = textfile_init, + .has_args = true, +}; + +void *textfile_init(int argc, char *argv[]) { + const char *dir = "."; // TODO: default + // TODO: parse arg + return must_strdup(dir); +} + +void textfile_collect(scrape_req *req, void *ctx) { + const char *dir = ctx; + + DIR *d = opendir(dir); + if (!d) + return; + + struct dirent *dent; + char buf[4096]; + + while ((dent = readdir(d))) { + size_t name_len = strlen(dent->d_name); + if (name_len < 6 || strcmp(dent->d_name + name_len - 5, ".prom") != 0) + continue; + + snprintf(buf, sizeof buf, "%s/%s", dir, dent->d_name); + FILE *f = fopen(buf, "r"); + if (!f) + continue; + + bool has_newline = true; + size_t len; + while ((len = fread(buf, 1, sizeof buf, f)) > 0) { + scrape_write_raw(req, buf, len); + has_newline = buf[len - 1] == '\n'; + } + if (!has_newline) + scrape_write_raw(req, (char[]){'\n'}, 1); + + fclose(f); + } + + closedir(d); +} diff --git a/textfile.h b/textfile.h new file mode 100644 index 0000000..45c86b0 --- /dev/null +++ b/textfile.h @@ -0,0 +1,11 @@ +#ifndef PNANOE_TEXTFILE_H_ +#define PNANOE_TEXTFILE_H_ 1 + +#include "collector.h" + +extern const struct collector textfile_collector; + +void *textfile_init(int argc, char *argv[]); +void textfile_collect(scrape_req *req, void *ctx); + +#endif // PNANOE_TEXTFILE_H_ diff --git a/uname.c b/uname.c new file mode 100644 index 0000000..6d71764 --- /dev/null +++ b/uname.c @@ -0,0 +1,55 @@ +#include <stdio.h> +#include <stdlib.h> +#include <sys/utsname.h> + +#include "uname.h" +#include "util.h" + +static void *uname_init(int argc, char *argv[]); +static void uname_collect(scrape_req *req, void *ctx); + +const struct collector uname_collector = { + .name = "uname", + .collect = uname_collect, + .init = uname_init, +}; + +enum { + label_machine, + label_nodename, + label_release, + label_sysname, + label_version, + max_label, +}; + +struct uname_context { + const char *labels[max_label + 1][2]; +}; + +static void *uname_init(int argc, char *argv[]) { + struct utsname name; + if (uname(&name) == -1) { + perror("uname"); + return 0; + } + + struct uname_context *ctx = must_malloc(sizeof *ctx); + ctx->labels[label_machine][0] = "machine"; + ctx->labels[label_machine][1] = must_strdup(name.machine); + ctx->labels[label_nodename][0] = "nodename"; + ctx->labels[label_nodename][1] = must_strdup(name.nodename); + ctx->labels[label_release][0] = "release"; + ctx->labels[label_release][1] = must_strdup(name.release); + ctx->labels[label_sysname][0] = "sysname"; + ctx->labels[label_sysname][1] = must_strdup(name.sysname); + ctx->labels[label_version][0] = "version"; + ctx->labels[label_version][1] = must_strdup(name.version); + ctx->labels[max_label][0] = ctx->labels[max_label][1] = 0; + return ctx; +} + +static void uname_collect(scrape_req *req, void *ctx_ptr) { + struct uname_context *ctx = ctx_ptr; + scrape_write(req, "node_uname_info", ctx->labels, 1.0); +} diff --git a/uname.h b/uname.h new file mode 100644 index 0000000..660b603 --- /dev/null +++ b/uname.h @@ -0,0 +1,8 @@ +#ifndef PNANOE_UNAME_H_ +#define PNANOE_UNAME_H_ 1 + +#include "collector.h" + +extern const struct collector uname_collector; + +#endif // PNANOE_UNAME_H_ diff --git a/util.c b/util.c new file mode 100644 index 0000000..6476f5c --- /dev/null +++ b/util.c @@ -0,0 +1,197 @@ +#define _POSIX_C_SOURCE 200809L + +#include <stdarg.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "util.h" + +// character buffers + +struct cbuf { + char *data; + size_t len; + size_t size; + size_t max_size; +}; + +static bool cbuf_reserve(cbuf *buf, size_t len) { + if (buf->len + len <= buf->size) + return true; + + size_t new_size = buf->size; + while (buf->len + len > new_size && new_size < buf->max_size) + new_size *= 2; + if (buf->len + len > new_size) + return false; + + char *new_data = must_realloc(buf->data, new_size); + buf->data = new_data; + buf->size = new_size; + return true; +} + +cbuf *cbuf_alloc(size_t initial_size, size_t max_size) { + cbuf *buf = must_malloc(sizeof *buf); + buf->data = must_malloc(initial_size); + buf->len = 0; + buf->size = initial_size; + buf->max_size = max_size; + return buf; +} + +void cbuf_reset(cbuf *buf) { + buf->len = 0; +} + +void cbuf_put(cbuf *buf, const void *src, size_t len) { + if (!cbuf_reserve(buf, len)) + return; + memcpy(buf->data + buf->len, src, len); + buf->len += len; +} + +void cbuf_puts(cbuf *buf, const char *src) { + cbuf_put(buf, src, strlen(src)); +} + +void cbuf_putc(cbuf *buf, int c) { + if (!cbuf_reserve(buf, 1)) + return; + buf->data[buf->len++] = c; +} + +void cbuf_putf(cbuf *buf, const char *fmt, ...) { + va_list ap; + + int len = 0; + va_start(ap, fmt); + len = vsnprintf(0, 0, fmt, ap); + va_end(ap); + + if (len > 0) { + if (!cbuf_reserve(buf, len + 1)) + return; + va_start(ap, fmt); + vsnprintf(buf->data + buf->len, len + 1, fmt, ap); + va_end(ap); + buf->len += len; + } +} + +const char *cbuf_get(struct cbuf *buf, size_t *len) { + *len = buf->len; + return buf->data; +} + +// string lists + +struct slist *slist_split(const char *str, const char *delim) { + struct slist *list = 0, **prev = &list, *next; + + while (*str) { + size_t span = strcspn(str, delim); + if (span == 0) { + str++; + continue; + } + + next = must_malloc(sizeof *next + span + 1); + memcpy(next->data, str, span); + next->data[span] = '\0'; + *prev = next; + prev = &next->next; + + str += span; + while (*str && strchr(delim, *str)) + str++; + } + *prev = 0; + + return list; +} + +struct slist **slist_append(struct slist **prev, const char *str) { + size_t len = strlen(str); + struct slist *new = *prev = must_malloc(sizeof *new + len + 1); + new->next = 0; + memcpy(new->data, str, len + 1); + return &new->next; +} + +struct slist *slist_prepend(struct slist *list, const char *str) { + size_t len = strlen(str); + struct slist *new = must_malloc(sizeof *new + len + 1); + new->next = list; + memcpy(new->data, str, len + 1); + return new; +} + +bool slist_contains(const struct slist *list, const char *key) { + for (; list; list = list->next) + if (strcmp(list->data, key) == 0) + return true; + return false; +} + +// miscellaneous utilities + +void *must_malloc(size_t size) { + void *ptr = malloc(size); + if (!ptr) { + perror("malloc"); + abort(); + } + return ptr; +} + +void *must_realloc(void *ptr, size_t size) { + void *new_ptr = realloc(ptr, size); + if (!new_ptr) { + perror("realloc"); + abort(); + } + return new_ptr; +} + +char *must_strdup(const char *src) { + char *dst = strdup(src); + if (!dst) { + perror("strdup"); + abort(); + } + return dst; +} + +char *fgets_line(char *s, int size, FILE *stream) { + s[size - 1] = '\0'; + char *got = fgets(s, size, stream); + if (!got) + return 0; + if (s[size - 1] == '\0' || s[size - 1] == '\n') + return got; + + int c; + while ((c = fgetc(stream)) != EOF) + if (c == '\n') + return got; + return 0; +} + +int write_all(int fd, const void *buf_ptr, size_t len) { + const char *buf = buf_ptr; + + while (len > 0) { + ssize_t wrote = write(fd, buf, len); + if (wrote <= 0) + return -1; + buf += wrote; + len -= wrote; + } + + return 0; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..3fa5fd0 --- /dev/null +++ b/util.h @@ -0,0 +1,62 @@ +#ifndef PNANOE_UTIL_H_ +#define PNANOE_UTIL_H_ 1 + +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> + +// character buffers + +typedef struct cbuf cbuf; + +cbuf *cbuf_alloc(size_t initial_size, size_t max_size); +void cbuf_reset(cbuf *buf); +void cbuf_put(cbuf *buf, const void *src, size_t len); +void cbuf_puts(cbuf *buf, const char *src); +void cbuf_putc(cbuf *buf, int c); +void cbuf_putf(cbuf *buf, const char *fmt, ...); +const char *cbuf_get(struct cbuf *buf, size_t *len); + +// string lists + +struct slist { + struct slist *next; + char data[]; +}; + +struct slist *slist_split(const char *str, const char *delim); +struct slist **slist_append(struct slist **prev, const char *str); +struct slist *slist_prepend(struct slist *list, const char *str); +bool slist_contains(const struct slist *list, const char *key); + +// miscellaneous utilities + +/** Calls `malloc(size)` and aborts if memory allocation failed. */ +void *must_malloc(size_t size); + +/** Calls `realloc(ptr, size)` and aborts if memory allocation failed. */ +void *must_realloc(void *ptr, size_t size); + +/** Calls `strdup(src)` and aborts if memory allocation failed. */ +char *must_strdup(const char *src); + +/** + * Reads a full line from \p stream into buffer \p s of size \p size. + * + * This function is otherwise identical to standard `fgets`, except that if a full line did not fit + * in the input buffer, characters are discarded from the stream up to and including the next + * newline character. If the file ends before a newline is encountered, a null pointer is + * required. This way this function always reads complete lines, which are just truncated if they + * don't fit in the buffer. + */ +char *fgets_line(char *s, int size, FILE *stream); + +/** + * Fully writes the contents of \p buf (\p len bytes) into file descriptor \p fd. + * + * If not all bytes could be written, this function just tries again. It returns 0 on success, or -1 + * if any of the `write` calls failed with an error. + */ +int write_all(int fd, const void *buf, size_t len); + +#endif // PNANOE_UTIL_H_ |