summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTor Brede Vekterli <vekterli@vespa.ai>2024-03-22 13:16:46 +0000
committerTor Brede Vekterli <vekterli@vespa.ai>2024-03-22 13:37:13 +0000
commite1923c6c1d60939deadf5a1bc04e8ff00c476155 (patch)
tree19ad66d203a272ababbb3328caadec8b27dcf58d
parent64d060cceccb4d567f56bbf2fbff93d994e3a52f (diff)
Add Prometheus support to simple-metrics snapshot rendering
-rw-r--r--vespalib/src/tests/metrics/simple_metrics_test.cpp23
-rw-r--r--vespalib/src/vespa/vespalib/metrics/CMakeLists.txt1
-rw-r--r--vespalib/src/vespa/vespalib/metrics/json_formatter.h8
-rw-r--r--vespalib/src/vespa/vespalib/metrics/producer.cpp27
-rw-r--r--vespalib/src/vespa/vespalib/metrics/prometheus_formatter.cpp153
-rw-r--r--vespalib/src/vespa/vespalib/metrics/prometheus_formatter.h40
6 files changed, 241 insertions, 11 deletions
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 <vespa/vespalib/metrics/simple_metrics_manager.h>
#include <vespa/vespalib/metrics/stable_store.h>
#include <vespa/vespalib/metrics/json_formatter.h>
+#include <vespa/vespalib/metrics/prometheus_formatter.h>
#include "mock_tick.h"
#include <stdio.h>
#include <unistd.h>
@@ -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 <vespa/vespalib/stllike/string.h>
#include <vespa/vespalib/data/slime/slime.h>
-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<MetricsManager> 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 <vespa/vespalib/stllike/asciistream.h>
+#include <algorithm>
+#include <cmath>
+
+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<uint64_t>(_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<const CounterSnapshot*> 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<std::pair<const GaugeSnapshot*, SubMetric>> 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 <vespa/vespalib/stllike/string.h>
+#include <string>
+
+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;
+};
+
+}