Current section
Files
Jump to
Current section
Files
c_src/prometheus_nif.cpp
// Prometheus text format parser NIF for TimelessMetrics
//
// Parses the entire Prometheus exposition body in one NIF call,
// returning {entries_list, error_count} to Elixir.
//
// Each entry: {metric_name_binary, labels_proplist, value_double, timestamp_int64}
// Lines without timestamps use 0 as sentinel.
#include <erl_nif.h>
#include <cstring>
#include <cstdlib>
#include <cmath>
// Parse a double from a string range. Returns true on success.
// Rejects Inf/NaN since BEAM doesn't support them as float values.
static bool parse_double(const char* start, const char* end, double* out) {
size_t len = end - start;
if (len == 0) return false;
// Reject special Prometheus values (BEAM can't represent them)
if (len >= 3) {
if (memcmp(start, "NaN", 3) == 0) return false;
if (memcmp(start, "Inf", 3) == 0) return false;
if (len >= 4 && (memcmp(start, "+Inf", 4) == 0 || memcmp(start, "-Inf", 4) == 0))
return false;
}
// Stack buffer for null-termination
char buf[64];
if (len >= sizeof(buf)) return false;
memcpy(buf, start, len);
buf[len] = '\0';
char* endp;
*out = strtod(buf, &endp);
if (endp == buf || *endp != '\0') return false;
// Double-check result isn't inf/nan (e.g., overflow)
if (std::isinf(*out) || std::isnan(*out)) return false;
return true;
}
// Parse a 64-bit integer from a string range. Returns true on success.
static bool parse_int64(const char* start, const char* end, int64_t* out) {
char buf[32];
size_t len = end - start;
if (len == 0 || len >= sizeof(buf)) return false;
memcpy(buf, start, len);
buf[len] = '\0';
char* endp;
*out = strtoll(buf, &endp, 10);
return endp != buf && *endp == '\0';
}
// Skip whitespace, returns pointer to first non-space char
static const char* skip_ws(const char* p, const char* end) {
while (p < end && (*p == ' ' || *p == '\t')) p++;
return p;
}
// Trim trailing whitespace, returns pointer past last non-space char
static const char* rtrim(const char* start, const char* end) {
while (end > start && (*(end-1) == ' ' || *(end-1) == '\t' || *(end-1) == '\r')) end--;
return end;
}
// Make an Erlang binary from a char range
static ERL_NIF_TERM make_binary(ErlNifEnv* env, const char* start, size_t len) {
ERL_NIF_TERM bin;
unsigned char* buf = enif_make_new_binary(env, len, &bin);
memcpy(buf, start, len);
return bin;
}
// Parse labels from "{key=\"val\",key2=\"val2\"}" format
// Returns a list of {key_binary, value_binary} tuples
static ERL_NIF_TERM parse_labels(ErlNifEnv* env, const char* start, const char* end) {
ERL_NIF_TERM list = enif_make_list(env, 0);
if (start >= end) return list;
// Build list in reverse, then return (proplist order doesn't matter for maps)
const char* p = start;
while (p < end) {
// Skip whitespace and commas
while (p < end && (*p == ' ' || *p == ',')) p++;
if (p >= end) break;
// Find '='
const char* eq = (const char*)memchr(p, '=', end - p);
if (!eq) break;
const char* key_start = p;
const char* key_end = eq;
// Trim trailing whitespace on key
while (key_end > key_start && *(key_end-1) == ' ') key_end--;
p = eq + 1;
// Expect '"' after '='
if (p >= end || *p != '"') break;
p++; // skip opening quote
// Find closing quote (handle escaped quotes)
const char* val_start = p;
while (p < end && *p != '"') {
if (*p == '\\' && p + 1 < end) {
p += 2; // skip escaped char
} else {
p++;
}
}
const char* val_end = p;
if (p < end) p++; // skip closing quote
ERL_NIF_TERM key_bin = make_binary(env, key_start, key_end - key_start);
ERL_NIF_TERM val_bin = make_binary(env, val_start, val_end - val_start);
ERL_NIF_TERM pair = enif_make_tuple2(env, key_bin, val_bin);
list = enif_make_list_cell(env, pair, list);
}
return list;
}
// Parse a single Prometheus line.
// Returns true on success and fills out the entry term.
static bool parse_line(ErlNifEnv* env, const char* line_start, const char* line_end,
ERL_NIF_TERM* entry) {
const char* p = skip_ws(line_start, line_end);
line_end = rtrim(p, line_end);
if (p >= line_end) return false;
// Skip comments and type/help lines
if (*p == '#') return false;
// Find metric name end (either '{' for labels or ' ' for no labels)
const char* metric_end = p;
while (metric_end < line_end && *metric_end != '{' && *metric_end != ' ' && *metric_end != '\t')
metric_end++;
if (metric_end == p || metric_end >= line_end) return false;
ERL_NIF_TERM metric_bin = make_binary(env, p, metric_end - p);
ERL_NIF_TERM labels;
const char* after_labels;
if (*metric_end == '{') {
// Has labels: find closing '}'
const char* brace_close = (const char*)memchr(metric_end + 1, '}', line_end - metric_end - 1);
if (!brace_close) return false;
labels = parse_labels(env, metric_end + 1, brace_close);
after_labels = brace_close + 1;
} else {
// No labels
labels = enif_make_list(env, 0);
after_labels = metric_end;
}
// Skip whitespace after metric/labels
after_labels = skip_ws(after_labels, line_end);
if (after_labels >= line_end) return false;
// Parse value
const char* value_start = after_labels;
const char* value_end = after_labels;
while (value_end < line_end && *value_end != ' ' && *value_end != '\t') value_end++;
double value;
if (!parse_double(value_start, value_end, &value)) return false;
// Parse optional timestamp
int64_t timestamp = 0;
const char* ts_start = skip_ws(value_end, line_end);
if (ts_start < line_end) {
const char* ts_end = ts_start;
while (ts_end < line_end && *ts_end != ' ' && *ts_end != '\t') ts_end++;
if (!parse_int64(ts_start, ts_end, ×tamp)) {
// Timestamp parse failure is not fatal; use 0 sentinel
timestamp = 0;
}
}
*entry = enif_make_tuple4(env, metric_bin, labels,
enif_make_double(env, value),
enif_make_int64(env, timestamp));
return true;
}
static ERL_NIF_TERM nif_parse(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary body;
if (!enif_inspect_binary(env, argv[0], &body)) {
return enif_make_badarg(env);
}
const char* data = (const char*)body.data;
size_t size = body.size;
const char* end = data + size;
ERL_NIF_TERM list = enif_make_list(env, 0);
int error_count = 0;
const char* line_start = data;
while (line_start < end) {
// Find end of line
const char* line_end = (const char*)memchr(line_start, '\n', end - line_start);
if (!line_end) line_end = end;
ERL_NIF_TERM entry;
if (parse_line(env, line_start, line_end, &entry)) {
list = enif_make_list_cell(env, entry, list);
} else {
// Check if it was a non-empty, non-comment line (actual parse error)
const char* p = skip_ws(line_start, line_end);
const char* trimmed_end = rtrim(p, line_end);
if (p < trimmed_end && *p != '#') {
error_count++;
}
}
line_start = line_end + 1;
}
// Reverse the list to preserve line order
ERL_NIF_TERM reversed;
if (!enif_make_reverse_list(env, list, &reversed)) {
return enif_make_badarg(env);
}
return enif_make_tuple2(env, reversed, enif_make_int(env, error_count));
}
static ErlNifFunc nif_funcs[] = {
{"parse", 1, nif_parse, ERL_NIF_DIRTY_JOB_CPU_BOUND}
};
ERL_NIF_INIT(Elixir.TimelessMetrics.PrometheusNif, nif_funcs, NULL, NULL, NULL, NULL)