Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions promon.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,18 @@ PrometheusParser::PrometheusParser()
// node_filesystem_avail_bytes{device="/dev/nvme0n1p2",fstype="ext4",mountpoint="/"} 8.650682368e+10
// xyz 3.14
auto ok = d_parser.load_grammar(R"(
ROOT <- ( ( ~COMMENTLINE / VLINE ) '\n')+
COMMENTLINE <- '#' (!'\n' .)*
VLINE <- KWORD SELS? ' ' VALUE
KWORD <- [a-zA-Z0-9_]+
SELS <- '{' KVPAIR (',' KVPAIR)* '}'
KVPAIR <- KWORD '=' '"' KVAL '"'
KVAL <- (!'"' .)*
VALUE <- [0-9.+e-]+
ROOT <- ( ( ~COMMENTLINE / VLINE )? '\n')*
COMMENTLINE <- '#' (!'\n' .)*
VLINE <- METRIC LABELS? VALUE ( ~TIMESTAMP )?
METRIC <- < [a-zA-Z_:][a-zA-Z0-9_:]* >
LABELS <- '{' LABELPAIR (',' LABELPAIR)* ','? '}'
LABELPAIR <- LABEL '=' < '"' LABELVALUE '"' >
LABEL <- < [a-zA-Z_][a-zA-Z0-9_]* >
LABELVALUE <- ('\\\\' / '\\"' / (!["] .))*
VALUE <- < [+-]? ([0-9.+e-]+ / 'inf'i / 'nan'i) >
TIMESTAMP <- < [0-9-]+ >

%whitespace <- [ \t]*
)" );

if(!ok)
Expand All @@ -50,15 +54,19 @@ VALUE <- [0-9.+e-]+
return atof(&vs.token()[0]);
};

d_parser["KVPAIR"] = [](const peg::SemanticValues &vs) {
d_parser["LABELPAIR"] = [](const peg::SemanticValues &vs) {
return std::make_pair(any_cast<string>(vs[0]), any_cast<string>(vs[1]));
};

d_parser["KWORD"] = [](const peg::SemanticValues &vs) {
d_parser["LABEL"] = [](const peg::SemanticValues &vs) {
return vs.token_to_string();
};

d_parser["KVAL"] = [](const peg::SemanticValues &vs) {
d_parser["METRIC"] = [](const peg::SemanticValues &vs) {
return vs.token_to_string();
};

d_parser["LABELVALUE"] = [](const peg::SemanticValues &vs) {
return vs.token_to_string();
};

Expand All @@ -74,7 +82,7 @@ VALUE <- [0-9.+e-]+
return ret;
};

d_parser["SELS"] = [](const peg::SemanticValues &vs) {
d_parser["LABELS"] = [](const peg::SemanticValues &vs) {
map<string,string> sels;
for(const auto& a : vs) {
sels.insert(any_cast<pair<string,string>>(a));
Expand All @@ -96,6 +104,10 @@ VALUE <- [0-9.+e-]+
*/
}

PrometheusParser::PrometheusParser(const peg::Log &log) : PrometheusParser() {
d_parser.set_logger(log);
}

void PrometheusParser::parse(const std::string& cont)
{
d_prom.clear();
Expand Down
1 change: 1 addition & 0 deletions simplomon.hh
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ class PrometheusParser
{
public:
PrometheusParser();
explicit PrometheusParser(const peg::Log& log);
void parse(const std::string& cont);

typedef std::map<std::string, std::map<std::map<std::string,std::string>, double>> prom_t;
Expand Down
91 changes: 91 additions & 0 deletions testrunner.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <algorithm> // std::move() and friends
#include <cmath>
#include <stdexcept>
#include <string>
#include <thread>
Expand All @@ -21,3 +22,93 @@ TEST_CASE("alert filter test") {
CHECK(1 == 1);
}

TEST_CASE("Prometheus parser") {
PrometheusParser parser([](auto...){});
using prom_t = PrometheusParser::prom_t;

SUBCASE("Empty input") {
CHECK_NOTHROW(parser.parse(""));
}

// Reference: https://prometheus.io/docs/instrumenting/exposition_formats/

SUBCASE("Empty lines are ignored") {
CHECK_NOTHROW(parser.parse("\n"));
CHECK_NOTHROW(parser.parse("\n\n"));
}

SUBCASE("Comment lines are ignored") {
CHECK_NOTHROW(parser.parse("# This is nothing\n"));
CHECK_EQ(parser.d_prom, prom_t {});

CHECK_NOTHROW(parser.parse(" # This is also nothing \n"));
CHECK_EQ(parser.d_prom, prom_t {});
}

SUBCASE("Simple line") {
CHECK_NOTHROW(parser.parse("\n# A comment:\nsome_metric 0.8\n"));
CHECK_EQ(parser.d_prom, prom_t {{"some_metric", {{{}, 0.8}}}});
}

SUBCASE("Special floating-point values") {
CHECK_NOTHROW(parser.parse("sandwich_bytes nan\n"));
CHECK(isnan(parser.d_prom["sandwich_bytes"][{}]));

CHECK_NOTHROW(parser.parse("sandwich_bytes -Inf\n"));
CHECK(isinf(parser.d_prom["sandwich_bytes"][{}]));
}

SUBCASE("Timestamps") {
CHECK_NOTHROW(parser.parse("temp_degrees 19.5 1712484057\n"));
CHECK_EQ(parser.d_prom, prom_t {{"temp_degrees", {{{}, 19.5}}}});
}

SUBCASE("Labels") {
CHECK_NOTHROW(parser.parse("backflips_total{method=\"normal\"} 12\n"));
CHECK_EQ(parser.d_prom, prom_t {{"backflips_total", {{{{"method", "normal"}}, 12}}}});

CHECK_NOTHROW(parser.parse("barrel_rolls_total{star=\"fox\",toad=\"slippy\"} 0\n"));
CHECK_EQ(parser.d_prom, prom_t {{"barrel_rolls_total", {{{{"star", "fox"}, {"toad", "slippy"}}, 0}}}});

CHECK_NOTHROW(parser.parse("trailing_commas_total{where=\"here\",} 1\n"));
CHECK_EQ(parser.d_prom, prom_t {{"trailing_commas_total", {{{{"where", "here"}}, 1}}}});

CHECK_NOTHROW(parser.parse(" space_info { size = \" space is big \" } 23 \n"));
CHECK_EQ(parser.d_prom, prom_t {{"space_info", {{{{"size", " space is big "}}, 23}}}});

// We accept, but do not unescape, escaped characters like backslashes and double quotes
CHECK_NOTHROW(parser.parse("escaped_labels_total{bs=\"\\\\\",q=\"\\\"\"} 1.0\n"));
CHECK_EQ(parser.d_prom, prom_t {{"escaped_labels_total", {{{{"bs", "\\\\"}, {"q", "\\\""}}, 1}}}});
}

SUBCASE("Example") {
const char *example = R"(
# HELP apt_upgrades_pending Apt packages pending updates by origin.
# TYPE apt_upgrades_pending gauge
apt_upgrades_pending{arch="all",origin="Debian:bookworm-security/stable-security"} 2
apt_upgrades_pending{arch="all",origin="Debian:bookworm/stable"} 4
apt_upgrades_pending{arch="amd64",origin="Debian:bookworm-security/stable-security"} 8
apt_upgrades_pending{arch="amd64",origin="Debian:bookworm/stable"} 24
)";

prom_t expected = {{"apt_upgrades_pending", {
{{{"arch", "all"}, {"origin", "Debian:bookworm-security/stable-security"}}, 2},
{{{"arch", "all"}, {"origin", "Debian:bookworm/stable"}}, 4},
{{{"arch", "amd64"}, {"origin", "Debian:bookworm-security/stable-security"}}, 8},
{{{"arch", "amd64"}, {"origin", "Debian:bookworm/stable"}}, 24}}}};

CHECK_NOTHROW(parser.parse(example));
CHECK_EQ(parser.d_prom, expected);
}

SUBCASE("Parse errors") {
CHECK_THROWS(parser.parse("line_without_terminator"));
CHECK_THROWS(parser.parse("value_is_garbage garbage\n"));
CHECK_THROWS(parser.parse("ok_value_bad_timestamp 382 bogus\n"));
CHECK_THROWS(parser.parse("bad!character!in!metric 48\n"));
CHECK_THROWS(parser.parse("bad_character{in!label=\"\"} 62\n"));
CHECK_THROWS(parser.parse("labels_empty{} 31\n"));
CHECK_THROWS(parser.parse("labels_lots_commas{,,,,,,,} 31\n"));
CHECK_THROWS(parser.parse("labels_missing_brace{foo=\"bar\" 51\n"));
}
}