From 33806e98e1e5fce1a19678d77feb28358e498224 Mon Sep 17 00:00:00 2001 From: Wander Nauta Date: Sun, 7 Apr 2024 11:11:43 +0200 Subject: [PATCH 1/3] Allow empty Prometheus responses This is consistent with upstream Prometheus: it is not an error to expose no metrics. --- promon.cc | 2 +- testrunner.cc | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/promon.cc b/promon.cc index 1f48f69..9556033 100644 --- a/promon.cc +++ b/promon.cc @@ -33,7 +33,7 @@ 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')+ +ROOT <- ( ( ~COMMENTLINE / VLINE ) '\n')* COMMENTLINE <- '#' (!'\n' .)* VLINE <- KWORD SELS? ' ' VALUE KWORD <- [a-zA-Z0-9_]+ diff --git a/testrunner.cc b/testrunner.cc index df35392..e25a7f5 100644 --- a/testrunner.cc +++ b/testrunner.cc @@ -21,3 +21,8 @@ TEST_CASE("alert filter test") { CHECK(1 == 1); } +TEST_CASE("Prometheus parser") { + PrometheusParser parser; + + parser.parse(""); +} \ No newline at end of file From 298829713eff51afee4184db0ffacc824ac1ca7a Mon Sep 17 00:00:00 2001 From: Wander Nauta Date: Sun, 7 Apr 2024 12:58:07 +0200 Subject: [PATCH 2/3] Make Prometheus parser grammar more lenient, add tests --- promon.cc | 32 ++++++++++++------- testrunner.cc | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 13 deletions(-) diff --git a/promon.cc b/promon.cc index 9556033..d94b658 100644 --- a/promon.cc +++ b/promon.cc @@ -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) @@ -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(vs[0]), any_cast(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(); }; @@ -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 sels; for(const auto& a : vs) { sels.insert(any_cast>(a)); diff --git a/testrunner.cc b/testrunner.cc index e25a7f5..985587d 100644 --- a/testrunner.cc +++ b/testrunner.cc @@ -1,5 +1,6 @@ #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include // std::move() and friends +#include #include #include #include @@ -23,6 +24,91 @@ TEST_CASE("alert filter test") { TEST_CASE("Prometheus parser") { PrometheusParser parser; + using prom_t = PrometheusParser::prom_t; - parser.parse(""); + 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")); + } } \ No newline at end of file From a450fdb580ff8837bddd5d6960cae6451334e203 Mon Sep 17 00:00:00 2001 From: Wander Nauta Date: Sun, 7 Apr 2024 13:09:07 +0200 Subject: [PATCH 3/3] Don't log parse errors in Prometheus parser test case --- promon.cc | 4 ++++ simplomon.hh | 1 + testrunner.cc | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/promon.cc b/promon.cc index d94b658..5db0b68 100644 --- a/promon.cc +++ b/promon.cc @@ -104,6 +104,10 @@ TIMESTAMP <- < [0-9-]+ > */ } +PrometheusParser::PrometheusParser(const peg::Log &log) : PrometheusParser() { + d_parser.set_logger(log); +} + void PrometheusParser::parse(const std::string& cont) { d_prom.clear(); diff --git a/simplomon.hh b/simplomon.hh index e65ef81..3ced18f 100644 --- a/simplomon.hh +++ b/simplomon.hh @@ -246,6 +246,7 @@ class PrometheusParser { public: PrometheusParser(); + explicit PrometheusParser(const peg::Log& log); void parse(const std::string& cont); typedef std::map, double>> prom_t; diff --git a/testrunner.cc b/testrunner.cc index 985587d..dfbb4d1 100644 --- a/testrunner.cc +++ b/testrunner.cc @@ -23,7 +23,7 @@ TEST_CASE("alert filter test") { } TEST_CASE("Prometheus parser") { - PrometheusParser parser; + PrometheusParser parser([](auto...){}); using prom_t = PrometheusParser::prom_t; SUBCASE("Empty input") {