diff --git a/core/src/utils/statistics/prometheus.cpp b/core/src/utils/statistics/prometheus.cpp index 4a895ca40f59..da9e1e5aaefd 100644 --- a/core/src/utils/statistics/prometheus.cpp +++ b/core/src/utils/statistics/prometheus.cpp @@ -23,6 +23,32 @@ namespace { enum class Typed { kYes, kNo }; +// Neutralizes a label value for the Prometheus text exposition format. The +// format only allows a backslash, a double quote and a line feed inside a +// quoted label value when they are escaped; a raw trailing backslash would +// escape the closing quote and pull following output into the value, and a raw +// line feed would start a new sample line. The double quote keeps the +// historical replacement with a single quote. +void AppendEscapedLabelValue(fmt::memory_buffer& buf, std::string_view value) { + for (const char c : value) { + switch (c) { + case '\\': + buf.push_back('\\'); + buf.push_back('\\'); + break; + case '\n': + buf.push_back('\\'); + buf.push_back('n'); + break; + case '"': + buf.push_back('\''); + break; + default: + buf.push_back(c); + } + } +} + template class FormatBuilder final : public utils::statistics::BaseFormatBuilder { public: @@ -141,8 +167,7 @@ class FormatBuilder final : public utils::statistics::BaseFormatBuilder { buf_.push_back(','); } fmt::format_to(std::back_inserter(buf_), FMT_COMPILE("{}=\""), impl::ToPrometheusLabel(label.Name())); - const auto& value = label.Value(); - std::ranges::replace_copy(value, std::back_inserter(buf_), '"', '\''); + AppendEscapedLabelValue(buf_, label.Value()); buf_.push_back('"'); sep = true; } diff --git a/core/src/utils/statistics/prometheus_test.cpp b/core/src/utils/statistics/prometheus_test.cpp index c0bd14f8eb72..7ec99df6dd0a 100644 --- a/core/src/utils/statistics/prometheus_test.cpp +++ b/core/src/utils/statistics/prometheus_test.cpp @@ -250,19 +250,43 @@ UTEST(Converter, SolomonChildrenLabelEscaping) { constexpr std::string_view expected = R"( # TYPE base_key_some_key_a_________g_test gauge -base_key_some_key_a_________g_test{application="processing",child_label____________name="label.value.#$/\ _{}''1"} 76 +base_key_some_key_a_________g_test{application="processing",child_label____________name="label.value.#$/\\ _{}''1"} 76 # TYPE base_key_some_key_a_________g_test1 gauge -base_key_some_key_a_________g_test1{application="processing",child_label____________name="label.value.#$/\ _{}''1"} 90 +base_key_some_key_a_________g_test1{application="processing",child_label____________name="label.value.#$/\\ _{}''1"} 90 # TYPE base_key_some_key_field1 gauge -base_key_some_key_field1{application="processing",child_label____________name="label.value.#$/\ _{}2"} 3 +base_key_some_key_field1{application="processing",child_label____________name="label.value.#$/\\ _{}2"} 3 # TYPE base_key_some_key_field2 gauge -base_key_some_key_field2{application="processing",child_label____________name="label.value.#$/\ _{}2"} 6.67 +base_key_some_key_field2{application="processing",child_label____________name="label.value.#$/\\ _{}2"} 6.67 # TYPE base_key_some_key_field3 gauge -base_key_some_key_field3{application="processing",overridden_label____________name="overridden.label.#$/\ _{}''value"} 9999 +base_key_some_key_field3{application="processing",overridden_label____________name="overridden.label.#$/\\ _{}''value"} 9999 )"; TestToMetricsPrometheus(statistics_storage, expected.substr(1), true); } +UTEST(MetricsPrometheus, LabelValueBackslashAndNewlineEscaped) { + auto producer = [](const utils::statistics::StatisticsRequest&) { + formats::json::ValueBuilder result; + utils::statistics::SolomonChildrenAreLabelValues(result, "label_name"); + // A label value with an embedded line feed and a trailing backslash. + result["a\nb\\"]["value"] = 1; + return result; + }; + + utils::statistics::Storage statistics_storage; + auto statistics_holder = statistics_storage.RegisterExtender("root", producer); + + const auto request = utils::statistics::Request::MakeWithPrefix({}, {{"application", "processing"}}); + const auto result = ToPrometheusFormat(statistics_storage, request); + + // The line feed must be written as `\n` and the trailing backslash doubled. + // Otherwise the raw backslash escapes the closing quote and the raw line + // feed starts a new sample line. + EXPECT_NE(result.find(R"(label_name="a\nb\\")"), std::string::npos) << result; + // A single gauge metric: only the `# TYPE` line and the sample line, so the + // escaped value must not add a third line feed. + EXPECT_EQ(std::ranges::count(result, '\n'), 2) << result; +} + UTEST(MetricsPrometheus, SimpleStatistics) { auto producer1 = [](const utils::statistics::StatisticsRequest&) { formats::json::ValueBuilder result;