From e1923c6c1d60939deadf5a1bc04e8ff00c476155 Mon Sep 17 00:00:00 2001 From: Tor Brede Vekterli Date: Fri, 22 Mar 2024 13:16:46 +0000 Subject: Add Prometheus support to simple-metrics snapshot rendering --- vespalib/src/tests/metrics/simple_metrics_test.cpp | 23 ++++ vespalib/src/vespa/vespalib/metrics/CMakeLists.txt | 1 + .../src/vespa/vespalib/metrics/json_formatter.h | 8 +- vespalib/src/vespa/vespalib/metrics/producer.cpp | 27 +++- .../vespalib/metrics/prometheus_formatter.cpp | 153 +++++++++++++++++++++ .../vespa/vespalib/metrics/prometheus_formatter.h | 40 ++++++ 6 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 vespalib/src/vespa/vespalib/metrics/prometheus_formatter.cpp create mode 100644 vespalib/src/vespa/vespalib/metrics/prometheus_formatter.h diff --git a/vespalib/src/tests/metrics/simple_metrics_test.cpp b/vespalib/src/tests/metrics/simple_metrics_test.cpp index 6003429904d..8d751bf3528 100644 --- a/vespalib/src/tests/metrics/simple_metrics_test.cpp +++ b/vespalib/src/tests/metrics/simple_metrics_test.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "mock_tick.h" #include #include @@ -116,6 +117,25 @@ void check_json(const vespalib::string &actual) EXPECT_TRUE(compare_json(expect, actual)); } +void check_prometheus(const vespalib::string &actual) { + vespalib::string expect = R"(foo 17 4500 +foo{chain="default",documenttype="music",thread="0"} 4 4500 +bar_count 4 4500 +bar_count{chain="vespa",documenttype="blogpost",thread="1"} 1 4500 +bar_count{chain="vespa",documenttype="blogpost",thread="2"} 1 4500 +bar_sum 168 4500 +bar_sum{chain="vespa",documenttype="blogpost",thread="1"} 14 4500 +bar_sum{chain="vespa",documenttype="blogpost",thread="2"} 11 4500 +bar_min 41 4500 +bar_min{chain="vespa",documenttype="blogpost",thread="1"} 14 4500 +bar_min{chain="vespa",documenttype="blogpost",thread="2"} 11 4500 +bar_max 43 4500 +bar_max{chain="vespa",documenttype="blogpost",thread="1"} 14 4500 +bar_max{chain="vespa",documenttype="blogpost",thread="2"} 11 4500 +)"; + EXPECT_EQUAL(expect, actual); +} + TEST("use simple_metrics_collector") { @@ -189,6 +209,9 @@ TEST("use simple_metrics_collector") JsonFormatter fmt2(snap2); check_json(fmt2.asString()); + PrometheusFormatter fmt3(snap2); + check_prometheus(fmt3.as_text_formatted()); + // flush sliding window for (int i = 5; i <= 10; ++i) { ticker->give(TimeStamp(i)); diff --git a/vespalib/src/vespa/vespalib/metrics/CMakeLists.txt b/vespalib/src/vespa/vespalib/metrics/CMakeLists.txt index d00e4b75d2a..80a3479294f 100644 --- a/vespalib/src/vespa/vespalib/metrics/CMakeLists.txt +++ b/vespalib/src/vespa/vespalib/metrics/CMakeLists.txt @@ -23,6 +23,7 @@ vespa_add_library(vespalib_vespalib_metrics OBJECT point_map_collection.cpp point_map.cpp producer.cpp + prometheus_formatter.cpp simple_metrics.cpp simple_metrics_manager.cpp simple_tick.cpp diff --git a/vespalib/src/vespa/vespalib/metrics/json_formatter.h b/vespalib/src/vespa/vespalib/metrics/json_formatter.h index 519359d0a42..ceb3b5d97fe 100644 --- a/vespalib/src/vespa/vespalib/metrics/json_formatter.h +++ b/vespalib/src/vespa/vespalib/metrics/json_formatter.h @@ -6,8 +6,7 @@ #include #include -namespace vespalib { -namespace metrics { +namespace vespalib::metrics { /** * utility for converting a snapshot to JSON format @@ -26,12 +25,11 @@ private: void handle(const CounterSnapshot &snapshot, Cursor &target); void handle(const GaugeSnapshot &snapshot, Cursor &target); public: - JsonFormatter(const Snapshot &snapshot); + explicit JsonFormatter(const Snapshot &snapshot); - vespalib::string asString() const { + [[nodiscard]] vespalib::string asString() const { return _data.toString(); } }; } // namespace vespalib::metrics -} // namespace vespalib diff --git a/vespalib/src/vespa/vespalib/metrics/producer.cpp b/vespalib/src/vespa/vespalib/metrics/producer.cpp index ca6d773e129..6f00bb23d48 100644 --- a/vespalib/src/vespa/vespalib/metrics/producer.cpp +++ b/vespalib/src/vespa/vespalib/metrics/producer.cpp @@ -3,6 +3,7 @@ #include "producer.h" #include "metrics_manager.h" #include "json_formatter.h" +#include "prometheus_formatter.h" namespace vespalib::metrics { @@ -12,20 +13,34 @@ Producer::Producer(std::shared_ptr m) Producer::~Producer() = default; +namespace { + +vespalib::string +format_snapshot(const Snapshot &snapshot, MetricsProducer::ExpositionFormat format) +{ + switch (format) { + case MetricsProducer::ExpositionFormat::JSON: + return JsonFormatter(snapshot).asString(); + case MetricsProducer::ExpositionFormat::Prometheus: + return PrometheusFormatter(snapshot).as_text_formatted(); + } + abort(); +} + +} + vespalib::string -Producer::getMetrics(const vespalib::string &, ExpositionFormat /*ignored*/) +Producer::getMetrics(const vespalib::string &, ExpositionFormat format) { Snapshot snap = _manager->snapshot(); - JsonFormatter fmt(snap); - return fmt.asString(); + return format_snapshot(snap, format); } vespalib::string -Producer::getTotalMetrics(const vespalib::string &, ExpositionFormat /*ignored*/) +Producer::getTotalMetrics(const vespalib::string &, ExpositionFormat format) { Snapshot snap = _manager->totalSnapshot(); - JsonFormatter fmt(snap); - return fmt.asString(); + return format_snapshot(snap, format); } } // namespace vespalib::metrics diff --git a/vespalib/src/vespa/vespalib/metrics/prometheus_formatter.cpp b/vespalib/src/vespa/vespalib/metrics/prometheus_formatter.cpp new file mode 100644 index 00000000000..4baa2216206 --- /dev/null +++ b/vespalib/src/vespa/vespalib/metrics/prometheus_formatter.cpp @@ -0,0 +1,153 @@ +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include "prometheus_formatter.h" +#include +#include +#include + +namespace vespalib::metrics { + +namespace { + +[[nodiscard]] constexpr bool valid_prometheus_char(char ch) noexcept { + // Prometheus also allows ':', but we don't. + return ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_'); +} + +void emit_prometheus_name(asciistream& out, stringref name) { + for (char ch : name) { + if (valid_prometheus_char(ch)) [[likely]] { + out << ch; + } else { + out << '_'; + } + } +} + +void emit_label_value(asciistream& out, stringref value) { + for (char ch : value) { + if (ch == '\\') { + out << "\\\\"; + } else if (ch == '\n') { + out << "\\n"; + } else if (ch == '\"') { + out << "\\\""; + } else [[likely]] { + out << ch; + } + } +} + +void emit_point_as_labels(asciistream& out, const PointSnapshot& point) { + if (point.dimensions.empty()) { + return; // No '{}' suffix if no dimensions are present. + } + out << '{'; + for (size_t i = 0; i < point.dimensions.size(); ++i) { + if (i > 0) { + out << ','; + } + auto& dim = point.dimensions[i]; + emit_prometheus_name(out, dim.dimensionName()); + out << "=\""; + emit_label_value(out, dim.labelValue()); + out << '"'; + } + out << '}'; +} + +void emit_sanitized_double(asciistream& out, double v) { + const bool inf = std::isinf(v); + const bool nan = std::isnan(v); + if (!inf && !nan) [[likely]] { + out << asciistream::Precision(16) << automatic << v; + } else if (inf) { + out << (v < 0.0 ? "-Inf" : "+Inf"); + } else { + out << "NaN"; + } +} + +} // anon ns + +PrometheusFormatter::PrometheusFormatter(const vespalib::metrics::Snapshot& snapshot) + : _snapshot(snapshot), + // TODO timestamp should be a chrono unit, not seconds as a double (here converted to millis explicitly) + _timestamp_str(std::to_string(static_cast(_snapshot.endTime() * 1000.0))) +{ +} + +PrometheusFormatter::~PrometheusFormatter() = default; + +void PrometheusFormatter::emit_counter(asciistream& out, const CounterSnapshot& cs) const { + emit_prometheus_name(out, cs.name()); + emit_point_as_labels(out, cs.point()); + out << ' ' << cs.count() << ' ' << _timestamp_str << '\n'; +} + +const char* PrometheusFormatter::sub_metric_type_str(SubMetric m) noexcept { + switch (m) { + case SubMetric::Count: return "count"; + case SubMetric::Sum: return "sum"; + case SubMetric::Min: return "min"; + case SubMetric::Max: return "max"; + } + abort(); +} + +void PrometheusFormatter::emit_gauge(asciistream& out, const GaugeSnapshot& gs, SubMetric m) const { + emit_prometheus_name(out, gs.name()); + out << '_' << sub_metric_type_str(m); + emit_point_as_labels(out, gs.point()); + out << ' '; + switch (m) { + case SubMetric::Count: out << gs.observedCount(); break; + case SubMetric::Sum: emit_sanitized_double(out, gs.sumValue()); break; + case SubMetric::Min: emit_sanitized_double(out, gs.minValue()); break; + case SubMetric::Max: emit_sanitized_double(out, gs.maxValue()); break; + } + out << ' ' << _timestamp_str << '\n'; +} + +void PrometheusFormatter::emit_counters(vespalib::asciistream& out) const { + std::vector ordered_counters; + ordered_counters.reserve(_snapshot.counters().size()); + for (const auto& cs : _snapshot.counters()) { + ordered_counters.emplace_back(&cs); // We expect instances to be stable during processing. + } + std::ranges::sort(ordered_counters, [](auto& lhs, auto& rhs) noexcept { + return lhs->name() < rhs->name(); + }); + for (const auto* cs : ordered_counters) { + emit_counter(out, *cs); + } +} + +void PrometheusFormatter::emit_gauges(vespalib::asciistream& out) const { + std::vector> ordered_gauges; + ordered_gauges.reserve(_snapshot.gauges().size() * NumSubMetrics); + for (const auto& gs : _snapshot.gauges()) { + ordered_gauges.emplace_back(&gs, SubMetric::Count); + ordered_gauges.emplace_back(&gs, SubMetric::Sum); + ordered_gauges.emplace_back(&gs, SubMetric::Min); + ordered_gauges.emplace_back(&gs, SubMetric::Max); + } + // Group all related time series together, ordered by name -> sub metric. + std::ranges::sort(ordered_gauges, [](auto& lhs, auto& rhs) noexcept { + if (lhs.first->name() != rhs.first->name()) { + return lhs.first->name() < rhs.first->name(); + } + return lhs.second < rhs.second; + }); + for (const auto& gs : ordered_gauges) { + emit_gauge(out, *gs.first, gs.second); + } +} + +vespalib::string PrometheusFormatter::as_text_formatted() const { + asciistream out; + emit_counters(out); + emit_gauges(out); + return out.str(); +} + +} // vespalib::metrics diff --git a/vespalib/src/vespa/vespalib/metrics/prometheus_formatter.h b/vespalib/src/vespa/vespalib/metrics/prometheus_formatter.h new file mode 100644 index 00000000000..57664ed0586 --- /dev/null +++ b/vespalib/src/vespa/vespalib/metrics/prometheus_formatter.h @@ -0,0 +1,40 @@ +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "snapshots.h" +#include +#include + +namespace vespalib { class asciistream; } + +namespace vespalib::metrics { + +/** + * Utility for formatting a metric Snapshot as Prometheus v0.0.4 text output. + * + * Note: we do not emit any `TYPE` information in the output, which means that + * all metrics are implicitly treated by the receiver as untyped. This is also + * the most conservative option since non-cumulative snapshots do not have + * monotonic counters, which violates Prometheus data model expectations. + */ +class PrometheusFormatter { + const Snapshot& _snapshot; + std::string _timestamp_str; +public: + explicit PrometheusFormatter(const Snapshot& snapshot); + ~PrometheusFormatter(); + + [[nodiscard]] vespalib::string as_text_formatted() const; +private: + enum SubMetric { Count, Sum, Min, Max }; + constexpr static uint32_t NumSubMetrics = 4; // Must match the enum cardinality of SubMetric + + [[nodiscard]] static const char* sub_metric_type_str(SubMetric m) noexcept; + + void emit_counter(vespalib::asciistream& out, const CounterSnapshot& cs) const; + void emit_counters(vespalib::asciistream& out) const; + void emit_gauge(vespalib::asciistream& out, const GaugeSnapshot& cs, SubMetric m) const; + void emit_gauges(vespalib::asciistream& out) const; +}; + +} -- cgit v1.2.3