From 71d9f344916c3a80ffb4458f7c1c0e674792b836 Mon Sep 17 00:00:00 2001 From: Karim Shamazov Date: Wed, 7 Oct 2026 19:56:31 +0300 Subject: [PATCH 1/8] [k2] use metrics descriptor API --- runtime-light/components/confdata/metrics.h | 45 +- .../confdata/state/instance-state.cpp | 28 +- .../confdata/state/instance-state.h | 6 +- runtime-light/k2-platform/k2-api.h | 84 +++- runtime-light/k2-platform/k2-header.h | 93 +++-- runtime-light/stdlib/diagnostics/metrics.h | 385 ++++++++---------- 6 files changed, 364 insertions(+), 277 deletions(-) diff --git a/runtime-light/components/confdata/metrics.h b/runtime-light/components/confdata/metrics.h index 1bc25d1c9e..260673b6c8 100644 --- a/runtime-light/components/confdata/metrics.h +++ b/runtime-light/components/confdata/metrics.h @@ -14,32 +14,39 @@ namespace kphp::confdata::metrics { // Each instance constructs these builders once, after its script allocator is ready. 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 +54,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/instance-state.cpp b/runtime-light/components/confdata/state/instance-state.cpp index d43c7999e8..6434698ef8 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](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) @@ -277,15 +271,9 @@ auto InstanceState::report_events_metrics(uint64_t timestamp) noexcept -> void { send(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] .send_count(count, timestamp) @@ -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](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}; diff --git a/runtime-light/components/confdata/state/instance-state.h b/runtime-light/components/confdata/state/instance-state.h index 85c09b9248..173214d7c2 100644 --- a/runtime-light/components/confdata/state/instance-state.h +++ b/runtime-light/components/confdata/state/instance-state.h @@ -82,9 +82,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..9b24882a2d 100644 --- a/runtime-light/k2-platform/k2-api.h +++ b/runtime-light/k2-platform/k2-api.h @@ -322,8 +322,88 @@ 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 StringView = K2StringView; +using LabelPair = K2LabelPair; + +inline std::expected metrics_register_counter(std::string_view name, std::span label_keys) noexcept { + k2::descriptor md{k2::INVALID_PLATFORM_DESCRIPTOR}; + if (auto error_code{k2_metrics_register_counter(&md, name.data(), name.size(), label_keys.data(), label_keys.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_keys) noexcept { + k2::descriptor md{k2::INVALID_PLATFORM_DESCRIPTOR}; + if (auto error_code{k2_metrics_register_gauge(&md, name.data(), name.size(), label_keys.data(), label_keys.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_keys) 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_keys.data(), label_keys.size())}; + error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return md; +} + +inline std::expected metrics_bind(k2::descriptor md, std::span label_pairs) noexcept { + k2::descriptor sd{k2::INVALID_PLATFORM_DESCRIPTOR}; + if (auto error_code{k2_metrics_bind(&sd, md, label_pairs.data(), label_pairs.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 label_pairs, + uint64_t timestamp_ns) noexcept { + if (auto error_code{k2_metrics_counter_add_with_labels(md, value, label_pairs.data(), label_pairs.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 label_pairs, + uint64_t timestamp_ns) noexcept { + if (auto error_code{k2_metrics_gauge_set_with_labels(md, value, label_pairs.data(), label_pairs.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 label_pairs, + uint64_t timestamp_ns) noexcept { + if (auto error_code{k2_metrics_histogram_observe_with_labels(md, value, label_pairs.data(), label_pairs.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..9fa0818b7b 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! @@ -631,30 +631,77 @@ 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); /** - * 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. + * A byte string: a pointer and a size. + */ +struct K2StringView { + const char* ptr; + size_t size; +}; + +/// A `(key, value)` label pair of a metric series. +struct K2LabelPair { + struct K2StringView key; + struct K2StringView value; +}; + +/** + * Metrics: a component registers each metric once (at initialization) and + * then updates it through descriptors. + * + * The registration fixes the metric name, its kind and its label schema + * (the set of label KEYS; 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 `(key, 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 `(key, value)` pairs of the series, for + * cases where the set of series is not known in advance. + * + * Contract (shared by all kinds): + * - buckets are explicit upper boundaries; `buckets_size == 0` + * registers a histogram with a default bucket layout; + * - descriptors are valid until the instance exits; there is no + * unregistration; registering the same name twice within one + * instance is an error; + * - `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` Out-pointers receiving the descriptor (0 on failure). + * @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 (keys only, the metric's label schema). + * @param `labels_size` (registration) The size of the array of label + * names. + * @param `buckets_ptr` A pointer to an array of buckets. + * @param `buckets_size` The size of the array of buckets. + * @param `label_pairs_ptr` (bind/updates) A pointer to an array of the + * `(key, value)` label pairs of the series. + * @param `label_pairs_size` (bind/updates) The size of the array of + * label pairs. * @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 K2StringView* 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 K2StringView* 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 K2StringView* labels_ptr, size_t labels_size); +int32_t k2_metrics_bind(uint64_t* sd, uint64_t md, const struct K2LabelPair* label_pairs_ptr, size_t label_pairs_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 K2LabelPair* label_pairs_ptr, size_t label_pairs_size, + uint64_t timestamp_ns); +int32_t k2_metrics_gauge_set_with_labels(uint64_t md, double value, const struct K2LabelPair* label_pairs_ptr, size_t label_pairs_size, uint64_t timestamp_ns); +int32_t k2_metrics_histogram_observe_with_labels(uint64_t md, double value, const struct K2LabelPair* label_pairs_ptr, size_t label_pairs_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..f8acac1113 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_strings(std::initializer_list strings) noexcept { + kphp::stl::vector k2_strings; + k2_strings.reserve(strings.size()); + for (std::string_view string : strings) { + k2_strings.emplace_back(k2::StringView{.ptr = string.data(), .size = string.size()}); + } + return k2_strings; +} + +inline kphp::stl::vector +make_k2_label_pairs(std::initializer_list> label_pairs) noexcept { + kphp::stl::vector k2_label_pairs; + k2_label_pairs.reserve(label_pairs.size()); + for (const auto& [key, value] : label_pairs) { + k2_label_pairs.emplace_back(k2::LabelPair{.key = {.ptr = key.data(), .size = key.size()}, .value = {.ptr = value.data(), .size = value.size()}}); + } + return k2_label_pairs; +} + +} // 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_keys = {}) noexcept { + auto k2_label_keys{impl::make_k2_strings(label_keys)}; + auto registered{k2::metrics_register_gauge(metric_name, k2_label_keys)}; + 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> label_pairs = {}) const noexcept { + if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return gauge_sender{k2::INVALID_PLATFORM_DESCRIPTOR}; + } + auto k2_label_pairs{impl::make_k2_label_pairs(label_pairs)}; + auto bound{k2::metrics_bind(m_metric, k2_label_pairs)}; + 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> label_pairs, + k2::SystemTime timestamp = {}) const noexcept { + if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return {}; + } + auto k2_label_pairs{impl::make_k2_label_pairs(label_pairs)}; + return k2::metrics_gauge_set_with_labels(m_metric, value, k2_label_pairs, 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_keys = {}) noexcept { + auto k2_label_keys{impl::make_k2_strings(label_keys)}; + auto registered{k2::metrics_register_counter(metric_name, k2_label_keys)}; + 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> label_pairs = {}) const noexcept { + if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return counter_sender{k2::INVALID_PLATFORM_DESCRIPTOR}; + } + auto k2_label_pairs{impl::make_k2_label_pairs(label_pairs)}; + auto bound{k2::metrics_bind(m_metric, k2_label_pairs)}; + 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> label_pairs, + k2::SystemTime timestamp = {}) const noexcept { + if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return {}; + } + auto k2_label_pairs{impl::make_k2_label_pairs(label_pairs)}; + return k2::metrics_counter_add_with_labels(m_metric, static_cast(count), k2_label_pairs, 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> label_pairs, + k2::SystemTime timestamp = {}) const noexcept { + return send_count(1, label_pairs, 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_keys = {}) noexcept { + auto k2_label_keys{impl::make_k2_strings(label_keys)}; + auto registered{k2::metrics_register_histogram(metric_name, buckets, k2_label_keys)}; + 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> label_pairs = {}) const noexcept { + if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return histogram_sender{k2::INVALID_PLATFORM_DESCRIPTOR}; + } + auto k2_label_pairs{impl::make_k2_label_pairs(label_pairs)}; + auto bound{k2::metrics_bind(m_metric, k2_label_pairs)}; + 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> label_pairs, + k2::SystemTime timestamp = {}) const noexcept { + if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { + return {}; + } + auto k2_label_pairs{impl::make_k2_label_pairs(label_pairs)}; + return k2::metrics_histogram_observe_with_labels(m_metric, value, k2_label_pairs, timestamp.since_epoch_ns); } }; + } // namespace kphp::diagnostics From edfee7ae49e9c67aa96b2ea139d3ef4f6176c9ba Mon Sep 17 00:00:00 2001 From: Karim Shamazov Date: Thu, 8 Oct 2026 15:13:41 +0300 Subject: [PATCH 2/8] [k2] register confdata metrics at component initialization The metric families and their bound series move from InstanceState to ComponentState: the metrics are component-scoped, every instance writes the same pre-bound series. Instances keep only the counters they flush into those series, and registration no longer repeats on every instance start. k2-header.h is synced with the component-scoped contract. --- runtime-light/components/confdata/metrics.h | 3 ++- .../confdata/state/component-state.h | 7 ++++++ .../confdata/state/instance-state.cpp | 24 +++++++++---------- .../confdata/state/instance-state.h | 6 +---- runtime-light/k2-platform/k2-header.h | 4 ++-- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/runtime-light/components/confdata/metrics.h b/runtime-light/components/confdata/metrics.h index 260673b6c8..226f829799 100644 --- a/runtime-light/components/confdata/metrics.h +++ b/runtime-light/components/confdata/metrics.h @@ -12,7 +12,8 @@ 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 { // 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"})}; 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 6434698ef8..06332d139d 100644 --- a/runtime-light/components/confdata/state/instance-state.cpp +++ b/runtime-light/components/confdata/state/instance-state.cpp @@ -256,7 +256,7 @@ auto InstanceState::metrics_loop() noexcept -> kphp::coro::task<> { } auto InstanceState::report_events_metrics(k2::SystemTime timestamp) noexcept -> void { - const auto send{[timestamp](kphp::diagnostics::counter_sender& sender, uint64_t& counter) noexcept { + 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) @@ -267,15 +267,15 @@ auto InstanceState::report_events_metrics(k2::SystemTime timestamp) noexcept -> }); }}; - 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(k2::SystemTime timestamp) noexcept -> void { for (size_t index{}; index < m_update_failure_counts.size(); ++index) { auto& counter{m_update_failure_counts[index]}; 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 { @@ -285,7 +285,7 @@ auto InstanceState::report_update_failure_metrics(k2::SystemTime timestamp) noex } 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); @@ -295,7 +295,7 @@ auto InstanceState::report_update_failure_metrics(k2::SystemTime timestamp) noex } auto InstanceState::report_capacity_metrics(k2::SystemTime timestamp) noexcept -> void { - const auto send{[timestamp](kphp::diagnostics::gauge_sender& sender, size_t value) noexcept { + 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}; @@ -305,16 +305,16 @@ auto InstanceState::report_capacity_metrics(k2::SystemTime timestamp) noexcept - 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 173214d7c2..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: diff --git a/runtime-light/k2-platform/k2-header.h b/runtime-light/k2-platform/k2-header.h index 9fa0818b7b..b10dbe7e56 100644 --- a/runtime-light/k2-platform/k2-header.h +++ b/runtime-light/k2-platform/k2-header.h @@ -666,9 +666,9 @@ struct K2LabelPair { * Contract (shared by all kinds): * - buckets are explicit upper boundaries; `buckets_size == 0` * registers a histogram with a default bucket layout; - * - descriptors are valid until the instance exits; there is no + * - descriptors are valid until the component exits; there is no * unregistration; registering the same name twice within one - * instance is an error; + * component is an error; * - `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) From fe5dcb9d748f81cd0d2ddc41ce628ad20ce55b34 Mon Sep 17 00:00:00 2001 From: Karim Shamazov Date: Thu, 8 Oct 2026 16:00:04 +0300 Subject: [PATCH 3/8] [k2] sync the metrics API availability contract in k2-header.h The metrics API is available from k2_create_component onwards; calls from image-level entry points fail with EINVAL. --- runtime-light/k2-platform/k2-header.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime-light/k2-platform/k2-header.h b/runtime-light/k2-platform/k2-header.h index b10dbe7e56..1acc92ca02 100644 --- a/runtime-light/k2-platform/k2-header.h +++ b/runtime-light/k2-platform/k2-header.h @@ -664,6 +664,9 @@ struct K2LabelPair { * cases where the set of series is not known in advance. * * Contract (shared by all kinds): + * - the metrics API is available from `k2_create_component` onwards; + * calls from image-level entry points (`k2_describe`, + * `k2_create_image`/`k2_init_image`) fail with `EINVAL`; * - buckets are explicit upper boundaries; `buckets_size == 0` * registers a histogram with a default bucket layout; * - descriptors are valid until the component exits; there is no From 5554dbe49710d1224aed3faeef5fecebdf728b0c Mon Sep 17 00:00:00 2001 From: Karim Shamazov Date: Fri, 9 Oct 2026 18:18:30 +0300 Subject: [PATCH 4/8] [k2] rename the metrics label ABI types after their role K2StringView/K2LabelPair become MetricLabelName/MetricLabelValue/ MetricLabel: a dedicated struct per role instead of one generic byte string, and "name" instead of "key" to match the monitoring system terminology. The k2-api wrappers and the diagnostics helpers follow; the contract doc block is updated to the new names and stays in sync with the k2-node header. --- runtime-light/k2-platform/k2-api.h | 35 +++++---- runtime-light/k2-platform/k2-header.h | 52 +++++++------ runtime-light/stdlib/diagnostics/metrics.h | 87 +++++++++++----------- 3 files changed, 86 insertions(+), 88 deletions(-) diff --git a/runtime-light/k2-platform/k2-api.h b/runtime-light/k2-platform/k2-api.h index 9b24882a2d..0c42d5a482 100644 --- a/runtime-light/k2-platform/k2-api.h +++ b/runtime-light/k2-platform/k2-api.h @@ -322,21 +322,22 @@ inline std::expected madvise(void* addr, size_t length, int32_t a return {}; } -using StringView = K2StringView; -using LabelPair = K2LabelPair; +using MetricLabelName = MetricLabelName; +using MetricLabelValue = MetricLabelValue; +using MetricLabel = MetricLabel; -inline std::expected metrics_register_counter(std::string_view name, std::span label_keys) noexcept { +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_keys.data(), label_keys.size())}; error_code != k2::errno_ok) + 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_keys) noexcept { +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_keys.data(), label_keys.size())}; error_code != k2::errno_ok) + 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}; } @@ -344,18 +345,18 @@ inline std::expected metrics_register_gauge(std::string } inline std::expected metrics_register_histogram(std::string_view name, std::span buckets, - std::span label_keys) noexcept { + 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_keys.data(), label_keys.size())}; + 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 label_pairs) noexcept { +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, label_pairs.data(), label_pairs.size())}; error_code != k2::errno_ok) [[unlikely]] { + 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; @@ -382,27 +383,25 @@ inline std::expected metrics_histogram_observe(k2::descriptor sd, return {}; } -inline std::expected metrics_counter_add_with_labels(k2::descriptor md, double value, std::span label_pairs, +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, label_pairs.data(), label_pairs.size(), timestamp_ns)}; error_code != k2::errno_ok) - [[unlikely]] { + 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 label_pairs, +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, label_pairs.data(), label_pairs.size(), timestamp_ns)}; error_code != k2::errno_ok) - [[unlikely]] { + 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 label_pairs, +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, label_pairs.data(), label_pairs.size(), timestamp_ns)}; error_code != k2::errno_ok) + 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}; } diff --git a/runtime-light/k2-platform/k2-header.h b/runtime-light/k2-platform/k2-header.h index 1acc92ca02..c71ea9e984 100644 --- a/runtime-light/k2-platform/k2-header.h +++ b/runtime-light/k2-platform/k2-header.h @@ -630,18 +630,22 @@ 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 byte string: a pointer and a size. - */ -struct K2StringView { +/// 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 `(key, value)` label pair of a metric series. -struct K2LabelPair { - struct K2StringView key; - struct K2StringView value; +/// A `(name, value)` label pair of a metric series. +struct MetricLabel { + struct MetricLabelName name; + struct MetricLabelValue value; }; /** @@ -649,18 +653,18 @@ struct K2LabelPair { * then updates it through descriptors. * * The registration fixes the metric name, its kind and its label schema - * (the set of label KEYS; values are supplied per series or per update). + * (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 `(key, value)` + * - 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 `(key, value)` pairs of the series, for + * 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): @@ -681,30 +685,24 @@ struct K2LabelPair { * @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 (keys only, the metric's label schema). - * @param `labels_size` (registration) The size of the array of label - * names. + * 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. - * @param `label_pairs_ptr` (bind/updates) A pointer to an array of the - * `(key, value)` label pairs of the series. - * @param `label_pairs_size` (bind/updates) The size of the array of - * label pairs. * @return returns 0 if everything is fine, otherwise error code */ -int32_t k2_metrics_register_counter(uint64_t* md, const char* name_ptr, size_t name_size, const struct K2StringView* 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 K2StringView* labels_ptr, size_t labels_size); +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 K2StringView* labels_ptr, size_t labels_size); -int32_t k2_metrics_bind(uint64_t* sd, uint64_t md, const struct K2LabelPair* label_pairs_ptr, size_t label_pairs_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 K2LabelPair* label_pairs_ptr, size_t label_pairs_size, - uint64_t timestamp_ns); -int32_t k2_metrics_gauge_set_with_labels(uint64_t md, double value, const struct K2LabelPair* label_pairs_ptr, size_t label_pairs_size, uint64_t timestamp_ns); -int32_t k2_metrics_histogram_observe_with_labels(uint64_t md, double value, const struct K2LabelPair* label_pairs_ptr, size_t label_pairs_size, - 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 f8acac1113..46f21fdef6 100644 --- a/runtime-light/stdlib/diagnostics/metrics.h +++ b/runtime-light/stdlib/diagnostics/metrics.h @@ -21,23 +21,24 @@ namespace kphp::diagnostics { namespace impl { -inline kphp::stl::vector make_k2_strings(std::initializer_list strings) noexcept { - kphp::stl::vector k2_strings; - k2_strings.reserve(strings.size()); - for (std::string_view string : strings) { - k2_strings.emplace_back(k2::StringView{.ptr = string.data(), .size = string.size()}); - } - return k2_strings; +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_label_pairs(std::initializer_list> label_pairs) noexcept { - kphp::stl::vector k2_label_pairs; - k2_label_pairs.reserve(label_pairs.size()); - for (const auto& [key, value] : label_pairs) { - k2_label_pairs.emplace_back(k2::LabelPair{.key = {.ptr = key.data(), .size = key.size()}, .value = {.ptr = value.data(), .size = value.size()}}); +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_label_pairs; + return k2_labels; } } // namespace impl @@ -80,9 +81,9 @@ struct gauge_metric final { : m_metric{metric} {} public: - static gauge_metric metric(std::string_view metric_name, std::initializer_list label_keys = {}) noexcept { - auto k2_label_keys{impl::make_k2_strings(label_keys)}; - auto registered{k2::metrics_register_gauge(metric_name, k2_label_keys)}; + 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}; @@ -90,12 +91,12 @@ struct gauge_metric final { return gauge_metric{*registered}; } - gauge_sender series(std::initializer_list> label_pairs = {}) const noexcept { + 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_label_pairs{impl::make_k2_label_pairs(label_pairs)}; - auto bound{k2::metrics_bind(m_metric, k2_label_pairs)}; + 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}; @@ -103,13 +104,13 @@ struct gauge_metric final { return gauge_sender{*bound}; } - std::expected send_value(double value, std::initializer_list> label_pairs, + 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_label_pairs{impl::make_k2_label_pairs(label_pairs)}; - return k2::metrics_gauge_set_with_labels(m_metric, value, k2_label_pairs, timestamp.since_epoch_ns); + 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); } }; @@ -143,9 +144,9 @@ struct counter_metric final { : m_metric{metric} {} public: - static counter_metric metric(std::string_view metric_name, std::initializer_list label_keys = {}) noexcept { - auto k2_label_keys{impl::make_k2_strings(label_keys)}; - auto registered{k2::metrics_register_counter(metric_name, k2_label_keys)}; + 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}; @@ -153,12 +154,12 @@ struct counter_metric final { return counter_metric{*registered}; } - counter_sender series(std::initializer_list> label_pairs = {}) const noexcept { + 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_label_pairs{impl::make_k2_label_pairs(label_pairs)}; - auto bound{k2::metrics_bind(m_metric, k2_label_pairs)}; + 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}; @@ -166,18 +167,18 @@ struct counter_metric final { return counter_sender{*bound}; } - std::expected send_count(uint32_t count, std::initializer_list> label_pairs, + 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_label_pairs{impl::make_k2_label_pairs(label_pairs)}; - return k2::metrics_counter_add_with_labels(m_metric, static_cast(count), k2_label_pairs, timestamp.since_epoch_ns); + 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); } - std::expected send_increment(std::initializer_list> label_pairs, + std::expected send_increment(std::initializer_list> labels, k2::SystemTime timestamp = {}) const noexcept { - return send_count(1, label_pairs, timestamp); + return send_count(1, labels, timestamp); } }; @@ -210,9 +211,9 @@ struct histogram_metric final { // `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_keys = {}) noexcept { - auto k2_label_keys{impl::make_k2_strings(label_keys)}; - auto registered{k2::metrics_register_histogram(metric_name, buckets, k2_label_keys)}; + 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}; @@ -220,12 +221,12 @@ struct histogram_metric final { return histogram_metric{*registered}; } - histogram_sender series(std::initializer_list> label_pairs = {}) const noexcept { + 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_label_pairs{impl::make_k2_label_pairs(label_pairs)}; - auto bound{k2::metrics_bind(m_metric, k2_label_pairs)}; + 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}; @@ -233,13 +234,13 @@ struct histogram_metric final { return histogram_sender{*bound}; } - std::expected send_observation(double value, std::initializer_list> label_pairs, + 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_label_pairs{impl::make_k2_label_pairs(label_pairs)}; - return k2::metrics_histogram_observe_with_labels(m_metric, value, k2_label_pairs, timestamp.since_epoch_ns); + 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); } }; From 8e23e6f6a1be545dd229c2953b4fe823e60fc4d6 Mon Sep 17 00:00:00 2001 From: Karim Shamazov Date: Thu, 15 Oct 2026 12:49:51 +0300 Subject: [PATCH 5/8] [k2] clarify the metrics lifecycle contract in k2-header.h Sync with the vkrs descriptor-api review fixes: registration is possible anywhere from k2_create_component onwards (not only at component initialization); descriptors are valid for the component's whole lifetime; md/sd are out-pointers only in the registration and bind functions, input values elsewhere. Co-Authored-By: Claude Code --- runtime-light/k2-platform/k2-header.h | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/runtime-light/k2-platform/k2-header.h b/runtime-light/k2-platform/k2-header.h index c71ea9e984..34dd7531b8 100644 --- a/runtime-light/k2-platform/k2-header.h +++ b/runtime-light/k2-platform/k2-header.h @@ -649,8 +649,8 @@ struct MetricLabel { }; /** - * Metrics: a component registers each metric once (at initialization) and - * then updates it through descriptors. + * Metrics: a component registers each metric once 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). @@ -668,20 +668,25 @@ struct MetricLabel { * cases where the set of series is not known in advance. * * Contract (shared by all kinds): - * - the metrics API is available from `k2_create_component` onwards; - * calls from image-level entry points (`k2_describe`, - * `k2_create_image`/`k2_init_image`) fail with `EINVAL`; + * - the metrics API is 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`; * - buckets are explicit upper boundaries; `buckets_size == 0` * registers a histogram with a default bucket layout; - * - descriptors are valid until the component exits; there is no - * unregistration; registering the same name twice within one + * - descriptors are valid for the component's whole lifetime; there + * is no unregistration; registering the same name twice within one * component is an error; * - `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` Out-pointers receiving the descriptor (0 on failure). + * @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 From 7985d755c807e7848b56339aefdfc4c2896a6584 Mon Sep 17 00:00:00 2001 From: Karim Shamazov Date: Thu, 15 Oct 2026 15:10:44 +0300 Subject: [PATCH 6/8] explicit empty labels set --- runtime-light/components/confdata/metrics.h | 2 +- runtime-light/stdlib/diagnostics/metrics.h | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/runtime-light/components/confdata/metrics.h b/runtime-light/components/confdata/metrics.h index 226f829799..54c9c10e69 100644 --- a/runtime-light/components/confdata/metrics.h +++ b/runtime-light/components/confdata/metrics.h @@ -42,7 +42,7 @@ struct events final { struct update_failures final { 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")}; + 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"}}), diff --git a/runtime-light/stdlib/diagnostics/metrics.h b/runtime-light/stdlib/diagnostics/metrics.h index 46f21fdef6..0c4527365d 100644 --- a/runtime-light/stdlib/diagnostics/metrics.h +++ b/runtime-light/stdlib/diagnostics/metrics.h @@ -81,7 +81,7 @@ struct gauge_metric final { : m_metric{metric} {} public: - static gauge_metric metric(std::string_view metric_name, std::initializer_list label_names = {}) noexcept { + 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]] { @@ -91,7 +91,7 @@ struct gauge_metric final { return gauge_metric{*registered}; } - gauge_sender series(std::initializer_list> labels = {}) const noexcept { + gauge_sender series(std::initializer_list> labels) const noexcept { if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { return gauge_sender{k2::INVALID_PLATFORM_DESCRIPTOR}; } @@ -144,7 +144,7 @@ struct counter_metric final { : m_metric{metric} {} public: - static counter_metric metric(std::string_view metric_name, std::initializer_list label_names = {}) noexcept { + 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]] { @@ -154,7 +154,7 @@ struct counter_metric final { return counter_metric{*registered}; } - counter_sender series(std::initializer_list> labels = {}) const noexcept { + counter_sender series(std::initializer_list> labels) const noexcept { if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { return counter_sender{k2::INVALID_PLATFORM_DESCRIPTOR}; } @@ -210,8 +210,7 @@ struct histogram_metric final { 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 { + 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]] { @@ -221,7 +220,7 @@ struct histogram_metric final { return histogram_metric{*registered}; } - histogram_sender series(std::initializer_list> labels = {}) const noexcept { + histogram_sender series(std::initializer_list> labels) const noexcept { if (m_metric == k2::INVALID_PLATFORM_DESCRIPTOR) [[unlikely]] { return histogram_sender{k2::INVALID_PLATFORM_DESCRIPTOR}; } From 198fdbba85c0eb6654a481d101b224dffba579fa Mon Sep 17 00:00:00 2001 From: Karim Shamazov Date: Thu, 15 Oct 2026 17:01:31 +0300 Subject: [PATCH 7/8] [k2] restrict metric registration and binding to component initialization --- runtime-light/k2-platform/k2-header.h | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/runtime-light/k2-platform/k2-header.h b/runtime-light/k2-platform/k2-header.h index 34dd7531b8..b50e9e4fb3 100644 --- a/runtime-light/k2-platform/k2-header.h +++ b/runtime-light/k2-platform/k2-header.h @@ -649,8 +649,8 @@ struct MetricLabel { }; /** - * Metrics: a component registers each metric once and then updates it - * through descriptors. + * 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). @@ -668,11 +668,14 @@ struct MetricLabel { * cases where the set of series is not known in advance. * * Contract (shared by all kinds): - * - the metrics API is available from `k2_create_component` onwards — - * at component initialization, at instance initialization and while - * serving requests; calls from image-level entry points + * - 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`; + * `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 From 93591da2efdb3be0053b26adcf37a95a8bf3dede Mon Sep 17 00:00:00 2001 From: Karim Shamazov Date: Mon, 19 Oct 2026 13:56:59 +0300 Subject: [PATCH 8/8] [k2] soften the duplicate metric registration contract The node does not reject a duplicate registration by itself: StatsHouse does not care, and the VictoriaMetrics backend in the shared mode returns the existing family on an identical re-registration. Only a conflicting re-registration may fail, depending on the monitoring system. Co-Authored-By: Claude Code --- runtime-light/k2-platform/k2-header.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/runtime-light/k2-platform/k2-header.h b/runtime-light/k2-platform/k2-header.h index b50e9e4fb3..7205f8e051 100644 --- a/runtime-light/k2-platform/k2-header.h +++ b/runtime-light/k2-platform/k2-header.h @@ -679,8 +679,11 @@ struct MetricLabel { * - 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; registering the same name twice within one - * component is an error; + * 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)