diff --git a/runtime-light/components/confdata/metrics.h b/runtime-light/components/confdata/metrics.h index 1bc25d1c9e..54c9c10e69 100644 --- a/runtime-light/components/confdata/metrics.h +++ b/runtime-light/components/confdata/metrics.h @@ -12,34 +12,42 @@ namespace kphp::confdata::metrics { -// Each instance constructs these builders once, after its script allocator is ready. +// The component constructs these once, at its initialization: the metrics are +// component-scoped, every instance writes the same pre-bound series. struct capacity final { - std::array m_memory{ - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_memory").tag("kind", "limit"), - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_memory").tag("kind", "used"), - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_memory").tag("kind", "real_used"), - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_memory").tag("kind", "oom_threshold"), + // The families are declared before the sender arrays: the senders bind their series on initialization. + kphp::diagnostics::gauge_metric m_memory_metric{kphp::diagnostics::gauge_metric::metric("k2_kphp_confdata_memory", {"kind"})}; + kphp::diagnostics::gauge_metric m_pieces_metric{kphp::diagnostics::gauge_metric::metric("k2_kphp_confdata_pieces", {"state"})}; + + std::array m_memory{ + m_memory_metric.series({{"kind", "limit"}}), + m_memory_metric.series({{"kind", "used"}}), + m_memory_metric.series({{"kind", "real_used"}}), + m_memory_metric.series({{"kind", "oom_threshold"}}), }; - std::array m_pieces{ - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_pieces").tag("state", "current"), - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_pieces").tag("state", "retired"), + std::array m_pieces{ + m_pieces_metric.series({{"state", "current"}}), + m_pieces_metric.series({{"state", "retired"}}), }; }; struct events final { - std::array m_events{ - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_events").tag("kind", "update"), - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_events").tag("kind", "delete"), + kphp::diagnostics::counter_metric m_events_metric{kphp::diagnostics::counter_metric::metric("k2_kphp_confdata_events", {"kind"})}; + + std::array m_events{ + m_events_metric.series({{"kind", "update"}}), + m_events_metric.series({{"kind", "delete"}}), }; }; struct update_failures final { - std::array m_failures{ - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_update_fails").tag("error", "transport"), - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_update_fails").tag("error", "old_offset"), - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_update_fails").tag("error", "malformed_response"), - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_update_fails").tag("error", "not_synced"), - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_update_fails").tag("error", "batch_rejected"), + kphp::diagnostics::counter_metric m_failures_metric{kphp::diagnostics::counter_metric::metric("k2_kphp_confdata_update_fails", {"error"})}; + kphp::diagnostics::gauge_metric m_old_offset_metric{kphp::diagnostics::gauge_metric::metric("k2_kphp_confdata_update_fails_old_offset", {})}; + + std::array m_failures{ + m_failures_metric.series({{"error", "transport"}}), m_failures_metric.series({{"error", "old_offset"}}), + m_failures_metric.series({{"error", "malformed_response"}}), m_failures_metric.series({{"error", "not_synced"}}), + m_failures_metric.series({{"error", "batch_rejected"}}), }; static_assert(std::to_underlying(kphp::confdata::subscribe_error::transport) == 0); static_assert(std::to_underlying(kphp::confdata::subscribe_error::old_offset) == 1); @@ -47,9 +55,7 @@ struct update_failures final { static_assert(std::to_underlying(kphp::confdata::subscribe_error::not_synced) == 3); static_assert(std::to_underlying(kphp::confdata::subscribe_error::batch_rejected) == 4); - kphp::diagnostics::metric_sender m_old_offset{ - kphp::diagnostics::metric_sender::metric("k2_kphp_confdata_update_fails_old_offset"), - }; + kphp::diagnostics::gauge_sender m_old_offset{m_old_offset_metric.series({})}; }; } // namespace kphp::confdata::metrics diff --git a/runtime-light/components/confdata/state/component-state.h b/runtime-light/components/confdata/state/component-state.h index 69355c8c7d..516e010a22 100644 --- a/runtime-light/components/confdata/state/component-state.h +++ b/runtime-light/components/confdata/state/component-state.h @@ -12,6 +12,7 @@ #include "runtime-common/core/allocator/script-allocator.h" #include "runtime-common/core/std/containers.h" #include "runtime-light/allocator/allocator-state.h" +#include "runtime-light/components/confdata/metrics.h" #include "runtime-light/k2-platform/k2-api.h" #include "runtime-light/stdlib/diagnostics/logs.h" @@ -49,6 +50,12 @@ struct ComponentState final : private vk::not_copyable { size_t m_initial_instance_memory_size{DEFAULT_INIT_INSTANCE_ALLOCATOR_SIZE}; size_t m_min_instance_extra_memory_size{DEFAULT_MIN_INSTANCE_EXTRA_MEMORY_SIZE}; + // The metric families are registered and their series are bound here, at component + // initialization: the metrics are component-scoped, all instances write the same series. + kphp::confdata::metrics::events m_events_metrics; + kphp::confdata::metrics::capacity m_capacity_metrics; + kphp::confdata::metrics::update_failures m_update_failure_metrics; + // === METHODS ================================================================================== ComponentState() noexcept; static auto get() noexcept -> const ComponentState&; diff --git a/runtime-light/components/confdata/state/instance-state.cpp b/runtime-light/components/confdata/state/instance-state.cpp index d43c7999e8..06332d139d 100644 --- a/runtime-light/components/confdata/state/instance-state.cpp +++ b/runtime-light/components/confdata/state/instance-state.cpp @@ -248,21 +248,15 @@ auto InstanceState::metrics_loop() noexcept -> kphp::coro::task<> { for (;;) { k2::SystemTime now{}; k2::system_time(std::addressof(now)); - report_events_metrics(now.since_epoch_ns); - report_capacity_metrics(now.since_epoch_ns); - report_update_failure_metrics(now.since_epoch_ns); + report_events_metrics(now); + report_capacity_metrics(now); + report_update_failure_metrics(now); co_await m_io_scheduler.schedule(CONFDATA_METRICS_INTERVAL); } } -auto InstanceState::report_events_metrics(uint64_t timestamp) noexcept -> void { - const auto send{[timestamp](kphp::diagnostics::metric_sender& sender, uint64_t& counter) noexcept { - // FIXME: needs to be fixed in platform - // Zero counts currently abort the platform's metrics batch flush. - if (counter == 0) { - return; - } - +auto InstanceState::report_events_metrics(k2::SystemTime timestamp) noexcept -> void { + const auto send{[timestamp](const kphp::diagnostics::counter_sender& sender, uint64_t& counter) noexcept { const auto count{static_cast(std::min(counter, std::numeric_limits::max()))}; std::ignore = sender.send_count(count, timestamp) @@ -273,21 +267,15 @@ auto InstanceState::report_events_metrics(uint64_t timestamp) noexcept -> void { }); }}; - send(m_events_metrics.m_events[0], m_update_events_count); - send(m_events_metrics.m_events[1], m_delete_events_count); + send(m_component_state.m_events_metrics.m_events[0], m_update_events_count); + send(m_component_state.m_events_metrics.m_events[1], m_delete_events_count); } -auto InstanceState::report_update_failure_metrics(uint64_t timestamp) noexcept -> void { +auto InstanceState::report_update_failure_metrics(k2::SystemTime timestamp) noexcept -> void { for (size_t index{}; index < m_update_failure_counts.size(); ++index) { auto& counter{m_update_failure_counts[index]}; - // FIXME: needs to be fixed in platform - // Zero counts currently abort the platform's metrics batch flush. - if (counter == 0) { - continue; - } - const auto count{static_cast(std::min(counter, std::numeric_limits::max()))}; - std::ignore = m_update_failure_metrics.m_failures[index] + std::ignore = m_component_state.m_update_failure_metrics.m_failures[index] .send_count(count, timestamp) .transform([&counter, count] noexcept { counter -= count; }) .or_else([](int32_t error) noexcept -> std::expected { @@ -297,7 +285,7 @@ auto InstanceState::report_update_failure_metrics(uint64_t timestamp) noexcept - } std::ignore = m_update_failure_old_offset.transform([this, timestamp](auto old_offset) noexcept { - return m_update_failure_metrics.m_old_offset.send_value(static_cast(old_offset), timestamp) + return m_component_state.m_update_failure_metrics.m_old_offset.send_value(static_cast(old_offset), timestamp) .transform([this] noexcept { m_update_failure_old_offset.reset(); }) .or_else([](int32_t error) noexcept -> std::expected { kphp::log::warning("failed to report 'old_offset' confdata update failure: error -> {}", error); @@ -306,8 +294,8 @@ auto InstanceState::report_update_failure_metrics(uint64_t timestamp) noexcept - }); } -auto InstanceState::report_capacity_metrics(uint64_t timestamp) noexcept -> void { - const auto send{[timestamp](kphp::diagnostics::metric_sender& sender, size_t value) noexcept { +auto InstanceState::report_capacity_metrics(k2::SystemTime timestamp) noexcept -> void { + const auto send{[timestamp](const kphp::diagnostics::gauge_sender& sender, size_t value) noexcept { std::ignore = sender.send_value(static_cast(value), timestamp).or_else([](int32_t error) noexcept -> std::expected { kphp::log::warning("failed to report confdata capacity metrics: error -> {}", error); return std::unexpected{error}; @@ -317,16 +305,16 @@ auto InstanceState::report_capacity_metrics(uint64_t timestamp) noexcept -> void if (!m_confdata_pieces.empty()) { auto& current{m_confdata_pieces.back().storage()}; const auto usage{current.memory_usage()}; - send(m_capacity_metrics.m_memory[0], usage.m_capacity); - send(m_capacity_metrics.m_memory[1], usage.m_allocated); - send(m_capacity_metrics.m_memory[2], usage.m_used); - send(m_capacity_metrics.m_memory[3], usage.m_oom_threshold); + send(m_component_state.m_capacity_metrics.m_memory[0], usage.m_capacity); + send(m_component_state.m_capacity_metrics.m_memory[1], usage.m_allocated); + send(m_component_state.m_capacity_metrics.m_memory[2], usage.m_used); + send(m_component_state.m_capacity_metrics.m_memory[3], usage.m_oom_threshold); } // These are tracked logical extents, including headers, not RSS or proof of K2 allocation release. const size_t current_count{m_confdata_pieces.empty() ? 0U : 1U}; - send(m_capacity_metrics.m_pieces[0], current_count); - send(m_capacity_metrics.m_pieces[1], m_confdata_pieces.size() - current_count); + send(m_component_state.m_capacity_metrics.m_pieces[0], current_count); + send(m_component_state.m_capacity_metrics.m_pieces[1], m_confdata_pieces.size() - current_count); } auto InstanceState::accept_loop() noexcept -> kphp::coro::task<> { diff --git a/runtime-light/components/confdata/state/instance-state.h b/runtime-light/components/confdata/state/instance-state.h index 85c09b9248..e700fb5fb0 100644 --- a/runtime-light/components/confdata/state/instance-state.h +++ b/runtime-light/components/confdata/state/instance-state.h @@ -56,12 +56,8 @@ struct InstanceState final : vk::not_copyable { uint64_t m_update_events_count{}; uint64_t m_delete_events_count{}; - kphp::confdata::metrics::events m_events_metrics; - kphp::confdata::metrics::capacity m_capacity_metrics; - - kphp::confdata::metrics::update_failures m_update_failure_metrics; - std::array> m_update_failure_counts{}; + std::array> m_update_failure_counts{}; std::optional m_update_failure_old_offset; // We only report the last old offset public: @@ -82,9 +78,9 @@ struct InstanceState final : vk::not_copyable { auto service_loop() noexcept -> kphp::coro::task<>; auto metrics_loop() noexcept -> kphp::coro::task<>; - auto report_events_metrics(uint64_t timestamp) noexcept -> void; - auto report_capacity_metrics(uint64_t timestamp) noexcept -> void; - auto report_update_failure_metrics(uint64_t timestamp) noexcept -> void; + auto report_events_metrics(k2::SystemTime timestamp) noexcept -> void; + auto report_capacity_metrics(k2::SystemTime timestamp) noexcept -> void; + auto report_update_failure_metrics(k2::SystemTime timestamp) noexcept -> void; auto release_reader(confdata_piece_list::iterator piece_it, kphp::confdata::storage::sample_id sample_id) noexcept -> void; auto serve_reader_lease(kphp::component::stream reader_stream) noexcept -> kphp::coro::task<>; diff --git a/runtime-light/k2-platform/k2-api.h b/runtime-light/k2-platform/k2-api.h index 768c2512f5..0c42d5a482 100644 --- a/runtime-light/k2-platform/k2-api.h +++ b/runtime-light/k2-platform/k2-api.h @@ -322,8 +322,87 @@ inline std::expected madvise(void* addr, size_t length, int32_t a return {}; } -inline std::expected write_metrics(std::span serialized_metric) noexcept { - if (auto error_code{k2_write_metrics(serialized_metric.data(), serialized_metric.size())}; error_code != k2::errno_ok) [[unlikely]] { +using MetricLabelName = MetricLabelName; +using MetricLabelValue = MetricLabelValue; +using MetricLabel = MetricLabel; + +inline std::expected metrics_register_counter(std::string_view name, std::span label_names) noexcept { + k2::descriptor md{k2::INVALID_PLATFORM_DESCRIPTOR}; + if (auto error_code{k2_metrics_register_counter(&md, name.data(), name.size(), label_names.data(), label_names.size())}; error_code != k2::errno_ok) + [[unlikely]] { + return std::unexpected{error_code}; + } + return md; +} + +inline std::expected metrics_register_gauge(std::string_view name, std::span label_names) noexcept { + k2::descriptor md{k2::INVALID_PLATFORM_DESCRIPTOR}; + if (auto error_code{k2_metrics_register_gauge(&md, name.data(), name.size(), label_names.data(), label_names.size())}; error_code != k2::errno_ok) + [[unlikely]] { + return std::unexpected{error_code}; + } + return md; +} + +inline std::expected metrics_register_histogram(std::string_view name, std::span buckets, + std::span label_names) noexcept { + k2::descriptor md{k2::INVALID_PLATFORM_DESCRIPTOR}; + if (auto error_code{k2_metrics_register_histogram(&md, name.data(), name.size(), buckets.data(), buckets.size(), label_names.data(), label_names.size())}; + error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return md; +} + +inline std::expected metrics_bind(k2::descriptor md, std::span labels) noexcept { + k2::descriptor sd{k2::INVALID_PLATFORM_DESCRIPTOR}; + if (auto error_code{k2_metrics_bind(&sd, md, labels.data(), labels.size())}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return sd; +} + +inline std::expected metrics_counter_add(k2::descriptor sd, double value, uint64_t timestamp_ns) noexcept { + if (auto error_code{k2_metrics_counter_add(sd, value, timestamp_ns)}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return {}; +} + +inline std::expected metrics_gauge_set(k2::descriptor sd, double value, uint64_t timestamp_ns) noexcept { + if (auto error_code{k2_metrics_gauge_set(sd, value, timestamp_ns)}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return {}; +} + +inline std::expected metrics_histogram_observe(k2::descriptor sd, double value, uint64_t timestamp_ns) noexcept { + if (auto error_code{k2_metrics_histogram_observe(sd, value, timestamp_ns)}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return {}; +} + +inline std::expected metrics_counter_add_with_labels(k2::descriptor md, double value, std::span labels, + uint64_t timestamp_ns) noexcept { + if (auto error_code{k2_metrics_counter_add_with_labels(md, value, labels.data(), labels.size(), timestamp_ns)}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return {}; +} + +inline std::expected metrics_gauge_set_with_labels(k2::descriptor md, double value, std::span labels, + uint64_t timestamp_ns) noexcept { + if (auto error_code{k2_metrics_gauge_set_with_labels(md, value, labels.data(), labels.size(), timestamp_ns)}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return {}; +} + +inline std::expected metrics_histogram_observe_with_labels(k2::descriptor md, double value, std::span labels, + uint64_t timestamp_ns) noexcept { + if (auto error_code{k2_metrics_histogram_observe_with_labels(md, value, labels.data(), labels.size(), timestamp_ns)}; error_code != k2::errno_ok) + [[unlikely]] { return std::unexpected{error_code}; } return {}; diff --git a/runtime-light/k2-platform/k2-header.h b/runtime-light/k2-platform/k2-header.h index 4a5b9a49b4..7205f8e051 100644 --- a/runtime-light/k2-platform/k2-header.h +++ b/runtime-light/k2-platform/k2-header.h @@ -28,7 +28,7 @@ #include #endif -#define K2_PLATFORM_HEADER_H_VERSION 16 +#define K2_PLATFORM_HEADER_H_VERSION 17 // Always check that enum value is a valid value! @@ -630,31 +630,90 @@ void* k2_mmap(uint64_t* md, void* addr, size_t len, int32_t prot, int32_t flags, */ int32_t k2_madvise(void* addr, size_t length, int32_t advise); +/// A label name: a byte string (a pointer and a size). +struct MetricLabelName { + const char* ptr; + size_t size; +}; + +/// A label value: a byte string (a pointer and a size). +struct MetricLabelValue { + const char* ptr; + size_t size; +}; + +/// A `(name, value)` label pair of a metric series. +struct MetricLabel { + struct MetricLabelName name; + struct MetricLabelValue value; +}; + /** - * Writes a pre-serialized metrics to the specified monitoring system. - * - * The buffer must contain a metric serialized according to the following format - * (TL serialization, native byte order): - * ... - * tag := - * - * value format: - * <`VALUE_MAGIC`:u32> - single double value - * <`VALUES_ARRAY_MAGIC`:u32>... - array of double values - * <`COUNT_MAGIC`:u32> - count value - * <`INC_MAGIC`:u32> - counter increment - * - * tl string is the standard TL string encoding. - * - * Multiple metrics can be sent in a single call by concatenating them sequentially: - * ... - * Each metric is serialized independently using the format described above. - * - * @param `buf` A pointer to the serialized metric(s) data. - * @param `buf_len` The length of the serialized metric(s) data in bytes. + * Metrics: a component registers each metric once at component + * initialization and then updates it through descriptors. + * + * The registration fixes the metric name, its kind and its label schema + * (the set of label names; values are supplied per series or per update). + * There is one registration function per kind; only the histogram + * registration carries kind options (explicit bucket boundaries). + * + * Two update styles: + * - bound: `k2_metrics_bind` fixes a series (a set of `(name, value)` + * label pairs) and returns a series descriptor; the + * `k2_metrics__*` functions then take just the series + * descriptor, the value and a timestamp — the hot path without any + * per-write string work; + * - dynamic: the `k2_metrics__*_with_labels` functions take the + * metric descriptor plus the `(name, value)` pairs of the series, for + * cases where the set of series is not known in advance. + * + * Contract (shared by all kinds): + * - registration and binding are available only during component + * initialization (`k2_create_component`/`k2_init_component`); the + * update functions are available from `k2_create_component` + * onwards — at component initialization, at instance initialization + * and while serving requests; calls from image-level entry points + * (`k2_describe`, `k2_create_image`/`k2_init_image`) fail with + * `EINVAL`, as do registration and binding outside component + * initialization; + * - buckets are explicit upper boundaries; `buckets_size == 0` + * registers a histogram with a default bucket layout; + * - descriptors are valid for the component's whole lifetime; there + * is no unregistration; a component is expected to register each + * metric once — a duplicate registration is not rejected by + * itself, but one conflicting with the existing metric (a + * different kind, label schema or buckets) may fail with + * `EINVAL`, depending on the monitoring system; + * - `timestamp_ns` is nanoseconds since epoch; `0` means "now" (the + * k2-node substitutes the current time); + * - a kind mismatch (e.g. `k2_metrics_gauge_set` on a counter series) + * is rejected with `EINVAL`, as is an unknown descriptor. + * + * @param `md`/`sd` The metric/series descriptor: the registration + * functions and `k2_metrics_bind` take an out-pointer receiving the + * descriptor (0 on failure); `k2_metrics_bind` and the update functions + * take the descriptor itself. + * @param `name_ptr` A pointer to the metric name (valid UTF-8). + * @param `name_size` The size of the metric name. + * @param `labels_ptr` (registration) A pointer to an array of label + * names (the metric's label schema); (bind/updates) a pointer to an + * array of the `(name, value)` label pairs of the series. + * @param `labels_size` The size of the `labels_ptr` array. + * @param `buckets_ptr` A pointer to an array of buckets. + * @param `buckets_size` The size of the array of buckets. * @return returns 0 if everything is fine, otherwise error code */ -int32_t k2_write_metrics(const void* buf, size_t buf_len); +int32_t k2_metrics_register_counter(uint64_t* md, const char* name_ptr, size_t name_size, const struct MetricLabelName* labels_ptr, size_t labels_size); +int32_t k2_metrics_register_gauge(uint64_t* md, const char* name_ptr, size_t name_size, const struct MetricLabelName* labels_ptr, size_t labels_size); +int32_t k2_metrics_register_histogram(uint64_t* md, const char* name_ptr, size_t name_size, const double* buckets_ptr, size_t buckets_size, + const struct MetricLabelName* labels_ptr, size_t labels_size); +int32_t k2_metrics_bind(uint64_t* sd, uint64_t md, const struct MetricLabel* labels_ptr, size_t labels_size); +int32_t k2_metrics_counter_add(uint64_t sd, double value, uint64_t timestamp_ns); +int32_t k2_metrics_gauge_set(uint64_t sd, double value, uint64_t timestamp_ns); +int32_t k2_metrics_histogram_observe(uint64_t sd, double value, uint64_t timestamp_ns); +int32_t k2_metrics_counter_add_with_labels(uint64_t md, double value, const struct MetricLabel* labels_ptr, size_t labels_size, uint64_t timestamp_ns); +int32_t k2_metrics_gauge_set_with_labels(uint64_t md, double value, const struct MetricLabel* labels_ptr, size_t labels_size, uint64_t timestamp_ns); +int32_t k2_metrics_histogram_observe_with_labels(uint64_t md, double value, const struct MetricLabel* labels_ptr, size_t labels_size, uint64_t timestamp_ns); /** * Sets `StreamStatus.please_whutdown_write=true` for the component on the diff --git a/runtime-light/stdlib/diagnostics/metrics.h b/runtime-light/stdlib/diagnostics/metrics.h index 6a0136f80e..0c4527365d 100644 --- a/runtime-light/stdlib/diagnostics/metrics.h +++ b/runtime-light/stdlib/diagnostics/metrics.h @@ -7,273 +7,240 @@ #include #include #include -#include -#include -#include +#include #include #include -#include #include -#include #include "runtime-common/core/allocator/script-allocator.h" #include "runtime-common/core/std/containers.h" #include "runtime-light/k2-platform/k2-api.h" -#include "runtime-light/tl/tl-core.h" -#include "runtime-light/tl/tl-types.h" +#include "runtime-light/stdlib/diagnostics/logs.h" -namespace tl { -struct metricValue final { - tl::f64 value{}; - - void store(tl::storer& tls) const noexcept { - value.store(tls); - } - - constexpr size_t footprint() const noexcept { - return value.footprint(); - } -}; - -struct metricValuesArray final { - std::span values; - - void store(tl::storer& tls) const noexcept { - tl::u32{static_cast(this->values.size())}.store(tls); - std::ranges::for_each(this->values, [&tls](const double& elem) noexcept { tl::f64{elem}.store(tls); }); - } +namespace kphp::diagnostics { - constexpr size_t footprint() const noexcept { - return std::ranges::fold_left(this->values, tl::u32{static_cast(this->values.size())}.footprint(), - [](size_t acc, const double& elem) noexcept { return acc + tl::f64{elem}.footprint(); }); - } -}; +namespace impl { + +inline kphp::stl::vector +make_k2_metric_label_names(std::initializer_list label_names) noexcept { + kphp::stl::vector k2_label_names; + k2_label_names.reserve(label_names.size()); + for (std::string_view label_name : label_names) { + k2_label_names.emplace_back(k2::MetricLabelName{.ptr = label_name.data(), .size = label_name.size()}); + } + return k2_label_names; +} + +inline kphp::stl::vector +make_k2_metric_labels(std::initializer_list> labels) noexcept { + kphp::stl::vector k2_labels; + k2_labels.reserve(labels.size()); + for (const auto& [name, value] : labels) { + k2_labels.emplace_back(k2::MetricLabel{.name = {.ptr = name.data(), .size = name.size()}, .value = {.ptr = value.data(), .size = value.size()}}); + } + return k2_labels; +} + +} // namespace impl + +// A metric family: the name and the label schema (the label KEYS) registered once at construction. +// Series of the family are created via `series(...)`; the label VALUES are named +// (`{{"key", "value"}, ...}`, in any order — the keys must cover the schema exactly). +// On a registration failure the error is reported once and the family +// becomes invalid — all its senders are no-ops. +// +// The senders write through a pre-bound series descriptor, so a send is a single FFI call +// without any string work. For series not known in advance the families also offer dynamic +// `send_*` overloads taking the label pairs per call (prefer bound senders on hot paths). +// `timestamp` is the wall-clock time of the sample; the default `k2::SystemTime{}` means "now" +// (the k2-node substitutes the current time). + +struct gauge_sender final { +private: + k2::descriptor m_series; -struct metricCount final { - tl::u32 count{}; + explicit gauge_sender(k2::descriptor series) noexcept + : m_series{series} {} - void store(tl::storer& tls) const noexcept { - count.store(tls); - } + friend struct gauge_metric; - constexpr size_t footprint() const noexcept { - return count.footprint(); +public: + std::expected send_value(double value, k2::SystemTime timestamp = {}) const noexcept { + if (m_series == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return {}; + } + return k2::metrics_gauge_set(m_series, value, timestamp.since_epoch_ns); } }; -struct metricInc final { - void store(tl::storer& /* tls */) const noexcept {} - - constexpr size_t footprint() const noexcept { - return 0; - } -}; +struct gauge_metric final { +private: + k2::descriptor m_metric; -class AnyMetricValue final { - static constexpr uint32_t VALUE_MAGIC = 0xcb5eb87U; - static constexpr uint32_t VALUES_ARRAY_MAGIC = 0xd4a59582U; - static constexpr uint32_t COUNT_MAGIC = 0x941bf7d1U; - static constexpr uint32_t INC_MAGIC = 0x23e305abU; + explicit gauge_metric(k2::descriptor metric) noexcept + : m_metric{metric} {} public: - std::variant value; - - void store(tl::storer& tls) const noexcept { - if (std::holds_alternative(value)) { - tl::magic{.value = AnyMetricValue::VALUE_MAGIC}.store(tls); - } else if (std::holds_alternative(value)) { - tl::magic{.value = AnyMetricValue::VALUES_ARRAY_MAGIC}.store(tls); - } else if (std::holds_alternative(value)) { - tl::magic{.value = AnyMetricValue::COUNT_MAGIC}.store(tls); - } else { - tl::magic{.value = AnyMetricValue::INC_MAGIC}.store(tls); + static gauge_metric metric(std::string_view metric_name, std::initializer_list label_names) noexcept { + auto k2_label_names{impl::make_k2_metric_label_names(label_names)}; + auto registered{k2::metrics_register_gauge(metric_name, k2_label_names)}; + if (!registered) [[unlikely]] { + kphp::log::warning("failed to register `{}` gauge: error -> {}", metric_name, registered.error()); + return gauge_metric{k2::INVALID_PLATFORM_DESCRIPTOR}; } - - std::visit([&tls](const auto& v) noexcept { v.store(tls); }, value); + return gauge_metric{*registered}; } - constexpr size_t footprint() const noexcept { - return tl::magic{}.footprint() + std::visit([](const auto& v) noexcept { return v.footprint(); }, value); - } -}; - -template -requires std::same_as>, tl::pair> -struct metric final { - tl::u64 timestamp{}; - tl::AnyMetricValue value{}; - tl::string metric_name{}; - TagRange tags{}; - - void store(tl::storer& tls) const noexcept - requires tl::serializable> - { - timestamp.store(tls); - metric_name.store(tls); - value.store(tls); - - tl::u32{.value = static_cast(std::ranges::distance(tags))}.store(tls); - std::ranges::for_each(tags, [&tls](const auto& elem) noexcept { elem.store(tls); }); + gauge_sender series(std::initializer_list> labels) const noexcept { + if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return gauge_sender{k2::INVALID_PLATFORM_DESCRIPTOR}; + } + auto k2_labels{impl::make_k2_metric_labels(labels)}; + auto bound{k2::metrics_bind(m_metric, k2_labels)}; + if (!bound) [[unlikely]] { + kphp::log::warning("failed to bind a series of gauge {}: error -> {}", m_metric, bound.error()); + return gauge_sender{k2::INVALID_PLATFORM_DESCRIPTOR}; + } + return gauge_sender{*bound}; } - constexpr size_t footprint() const noexcept - requires tl::footprintable> - { - return timestamp.footprint() + value.footprint() + metric_name.footprint() + - std::ranges::fold_left(tags, tl::u32{.value = static_cast(std::ranges::distance(tags))}.footprint(), - [](size_t acc, const auto& elem) noexcept { return acc + elem.footprint(); }); + std::expected send_value(double value, std::initializer_list> labels, + k2::SystemTime timestamp = {}) const noexcept { + if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return {}; + } + auto k2_labels{impl::make_k2_metric_labels(labels)}; + return k2::metrics_gauge_set_with_labels(m_metric, value, k2_labels, timestamp.since_epoch_ns); } }; -} // namespace tl -// --------------------------------------------------------------------------------------------------------- - -namespace kphp::diagnostics { -template -concept tag_range = std::ranges::range && std::is_constructible_v, std::ranges::range_value_t>; - -struct metric final { +struct counter_sender final { private: - tl::storer tls; - - metric() noexcept - : tls{0} {} + k2::descriptor m_series; - explicit metric(tl::storer&& tls) noexcept - : tls{std::move(tls)} {} + explicit counter_sender(k2::descriptor series) noexcept + : m_series{series} {} - static uint64_t ns_timestamp_now() noexcept { - k2::SystemTime st{}; - k2::system_time(std::addressof(st)); - return st.since_epoch_ns; - } + friend struct counter_metric; - std::expected send() const noexcept { - return k2::write_metrics(this->tls.view()); +public: + std::expected send_count(uint32_t count, k2::SystemTime timestamp = {}) const noexcept { + if (m_series == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return {}; + } + return k2::metrics_counter_add(m_series, static_cast(count), timestamp.since_epoch_ns); } - // clears buffer and returns it with preserved capacity for reuse by metric::with_buffer() - std::pair> send() && noexcept { - std::expected send_result{k2::write_metrics(this->tls.view())}; - this->tls.clear(); - return std::pair{std::move(this->tls), std::move(send_result)}; + std::expected send_increment(k2::SystemTime timestamp = {}) const noexcept { + return send_count(1, timestamp); } +}; - template - decltype(auto) build_and_send(this Self&& self, std::string_view metric_name, TagRange&& tags, tl::AnyMetricValue value, - std::optional timestamp) noexcept { - self.tls.clear(); - - uint64_t ns_timestamp{timestamp.value_or(metric::ns_timestamp_now())}; - tl::metric serialized{.timestamp = tl::u64{ns_timestamp}, - .value = value, - .metric_name = tl::string{metric_name}, - .tags = std::forward(tags) | std::views::transform([](const auto& elem) noexcept -> tl::pair { - std::pair sv_pair{elem}; - return tl::pair{std::pair{tl::string{sv_pair.first}, tl::string{sv_pair.second}}}; - })}; +struct counter_metric final { +private: + k2::descriptor m_metric; - self.tls.reserve(serialized.footprint()); - serialized.store(self.tls); - - return std::forward(self).send(); - } + explicit counter_metric(k2::descriptor metric) noexcept + : m_metric{metric} {} public: - static metric empty() noexcept { - return metric{}; - } - - static metric with_buffer(tl::storer&& tls) noexcept { - return metric{std::move(tls)}; - } - - template - auto send_value(this Self&& self, std::string_view metric_name, TagRange&& tags, double value, std::optional timestamp = std::nullopt) noexcept { - return std::forward(self).build_and_send(metric_name, std::forward(tags), tl::AnyMetricValue{tl::metricValue{tl::f64{value}}}, timestamp); + static counter_metric metric(std::string_view metric_name, std::initializer_list label_names) noexcept { + auto k2_label_names{impl::make_k2_metric_label_names(label_names)}; + auto registered{k2::metrics_register_counter(metric_name, k2_label_names)}; + if (!registered) [[unlikely]] { + kphp::log::warning("failed to register `{}` counter: error -> {}", metric_name, registered.error()); + return counter_metric{k2::INVALID_PLATFORM_DESCRIPTOR}; + } + return counter_metric{*registered}; } - template - auto send_values_array(this Self&& self, std::string_view metric_name, TagRange&& tags, std::span values, - std::optional timestamp = std::nullopt) noexcept { - return std::forward(self).build_and_send(metric_name, std::forward(tags), tl::AnyMetricValue{tl::metricValuesArray{values}}, timestamp); + counter_sender series(std::initializer_list> labels) const noexcept { + if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return counter_sender{k2::INVALID_PLATFORM_DESCRIPTOR}; + } + auto k2_labels{impl::make_k2_metric_labels(labels)}; + auto bound{k2::metrics_bind(m_metric, k2_labels)}; + if (!bound) [[unlikely]] { + kphp::log::warning("failed to bind a series of counter {}: error -> {}", m_metric, bound.error()); + return counter_sender{k2::INVALID_PLATFORM_DESCRIPTOR}; + } + return counter_sender{*bound}; } - template - auto send_count(this Self&& self, std::string_view metric_name, TagRange&& tags, uint32_t count, std::optional timestamp = std::nullopt) noexcept { - return std::forward(self).build_and_send(metric_name, std::forward(tags), tl::AnyMetricValue{tl::metricCount{tl::u32{count}}}, timestamp); + std::expected send_count(uint32_t count, std::initializer_list> labels, + k2::SystemTime timestamp = {}) const noexcept { + if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return {}; + } + auto k2_labels{impl::make_k2_metric_labels(labels)}; + return k2::metrics_counter_add_with_labels(m_metric, static_cast(count), k2_labels, timestamp.since_epoch_ns); } - template - auto send_increment(this Self&& self, std::string_view metric_name, TagRange&& tags, std::optional timestamp = std::nullopt) noexcept { - return std::forward(self).build_and_send(metric_name, std::forward(tags), tl::AnyMetricValue{tl::metricInc{}}, timestamp); + std::expected send_increment(std::initializer_list> labels, + k2::SystemTime timestamp = {}) const noexcept { + return send_count(1, labels, timestamp); } }; -// --------------------------------------------------------------------------------------------------------- - -// If metric_sender is used as a non-const object, send_* calls reuse an internal buffer across calls, avoiding a new allocation each time. -// If it's const, each send_* call allocates a fresh buffer. -struct metric_sender final { +struct histogram_sender final { private: - kphp::stl::string metric_name; - kphp::stl::vector, kphp::stl::string>, - kphp::memory::script_allocator> - tags; - tl::storer buffer; + k2::descriptor m_series; - explicit metric_sender(std::string_view metric_name) noexcept - : metric_name{metric_name} {} + explicit histogram_sender(k2::descriptor series) noexcept + : m_series{series} {} -public: - static metric_sender metric(std::string_view metric_name) noexcept { - return metric_sender{metric_name}; - } - - template - auto tag(this Self&& self, std::string_view tag_name, std::string_view tag_value) noexcept -> Self&& { - self.tags.emplace_back(tag_name, tag_value); - return std::forward(self); - } + friend struct histogram_metric; - auto send_value(double value, std::optional timestamp = std::nullopt) const noexcept { - return metric::empty().send_value(this->metric_name, this->tags, value, timestamp); - } - - auto send_values_array(std::span values, std::optional timestamp = std::nullopt) const noexcept { - return metric::empty().send_values_array(this->metric_name, this->tags, values, timestamp); - } - - auto send_count(uint32_t count, std::optional timestamp = std::nullopt) const noexcept { - return metric::empty().send_count(this->metric_name, this->tags, count, timestamp); +public: + std::expected send_observation(double value, k2::SystemTime timestamp = {}) const noexcept { + if (m_series == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return {}; + } + return k2::metrics_histogram_observe(m_series, value, timestamp.since_epoch_ns); } +}; - auto send_increment(std::optional timestamp = std::nullopt) const noexcept { - return metric::empty().send_increment(this->metric_name, this->tags, timestamp); - } +struct histogram_metric final { +private: + k2::descriptor m_metric; - auto send_value(double value, std::optional timestamp = std::nullopt) noexcept { - auto [buffer, result]{metric::with_buffer(std::move(this->buffer)).send_value(this->metric_name, this->tags, value, timestamp)}; - this->buffer = std::move(buffer); - return result; - } + explicit histogram_metric(k2::descriptor metric) noexcept + : m_metric{metric} {} - auto send_values_array(std::span values, std::optional timestamp = std::nullopt) noexcept { - auto [buffer, result]{metric::with_buffer(std::move(this->buffer)).send_values_array(this->metric_name, this->tags, values, timestamp)}; - this->buffer = std::move(buffer); - return result; +public: + // `buckets` are the explicit upper (`le`) bucket boundaries (finite, strictly ascending); + // empty registers a histogram without an explicit layout (the monitoring system assigns a default one). + static histogram_metric metric(std::string_view metric_name, std::span buckets, std::initializer_list label_names) noexcept { + auto k2_label_names{impl::make_k2_metric_label_names(label_names)}; + auto registered{k2::metrics_register_histogram(metric_name, buckets, k2_label_names)}; + if (!registered) [[unlikely]] { + kphp::log::warning("failed to register `{}` histogram: error -> {}", metric_name, registered.error()); + return histogram_metric{k2::INVALID_PLATFORM_DESCRIPTOR}; + } + return histogram_metric{*registered}; } - auto send_count(uint32_t count, std::optional timestamp = std::nullopt) noexcept { - auto [buffer, result]{metric::with_buffer(std::move(this->buffer)).send_count(this->metric_name, this->tags, count, timestamp)}; - this->buffer = std::move(buffer); - return result; + histogram_sender series(std::initializer_list> labels) const noexcept { + if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return histogram_sender{k2::INVALID_PLATFORM_DESCRIPTOR}; + } + auto k2_labels{impl::make_k2_metric_labels(labels)}; + auto bound{k2::metrics_bind(m_metric, k2_labels)}; + if (!bound) [[unlikely]] { + kphp::log::warning("failed to bind a series of histogram {}: error -> {}", m_metric, bound.error()); + return histogram_sender{k2::INVALID_PLATFORM_DESCRIPTOR}; + } + return histogram_sender{*bound}; } - auto send_increment(std::optional timestamp = std::nullopt) noexcept { - auto [buffer, result]{metric::with_buffer(std::move(this->buffer)).send_increment(this->metric_name, this->tags, timestamp)}; - this->buffer = std::move(buffer); - return result; + std::expected send_observation(double value, std::initializer_list> labels, + k2::SystemTime timestamp = {}) const noexcept { + if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return {}; + } + auto k2_labels{impl::make_k2_metric_labels(labels)}; + return k2::metrics_histogram_observe_with_labels(m_metric, value, k2_labels, timestamp.since_epoch_ns); } }; + } // namespace kphp::diagnostics