about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--collector.h15
-rw-r--r--cpu.c129
-rw-r--r--cpu.h8
-rw-r--r--hwmon.c200
-rw-r--r--hwmon.h8
-rw-r--r--main.c139
-rw-r--r--network.c186
-rw-r--r--network.h8
-rw-r--r--scrape.c152
-rw-r--r--scrape.h35
-rw-r--r--textfile.c59
-rw-r--r--textfile.h11
-rw-r--r--uname.c55
-rw-r--r--uname.h8
-rw-r--r--util.c197
-rw-r--r--util.h62
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_