diff --git a/runtime-light/k2-platform/k2-api.h b/runtime-light/k2-platform/k2-api.h index 617dad5e3f..22656b7381 100644 --- a/runtime-light/k2-platform/k2-api.h +++ b/runtime-light/k2-platform/k2-api.h @@ -245,6 +245,13 @@ 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]] { + return std::unexpected{error_code}; + } + return {}; +} + inline void please_shutdown(k2::descriptor descriptor) noexcept { k2_please_shutdown(descriptor); } diff --git a/runtime-light/k2-platform/k2-header.h b/runtime-light/k2-platform/k2-header.h index 25b8789f7f..1dd9f13b7d 100644 --- a/runtime-light/k2-platform/k2-header.h +++ b/runtime-light/k2-platform/k2-header.h @@ -383,6 +383,32 @@ void* k2_mmap(uint64_t* md, void* addr, size_t length, int32_t prot, int32_t fla */ 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. + * @return returns 0 if everything is fine, otherwise error code + */ +int32_t k2_write_metrics(const void* buf, size_t buf_len); + /** * Sets `StreamStatus.please_whutdown_write=true` for the component on the * opposite side (does not affect `StreamStatus` on your side). diff --git a/runtime-light/stdlib/diagnostics/metric.tl b/runtime-light/stdlib/diagnostics/metric.tl new file mode 100644 index 0000000000..1dbed73579 --- /dev/null +++ b/runtime-light/stdlib/diagnostics/metric.tl @@ -0,0 +1,13 @@ +---types--- + +pair {X:Type} {Y:Type} a:X b:Y = Pair X Y; +span#dad3ae87 {t:Type} count:# data:count*[t] = Span t; + +// Metric + +metricValue#0cb5eb87 value:double = AnyMetricValue; +metricValuesArray#d4a59582 values:%(Span double) = AnyMetricValue; +metricCount#941bf7d1 count:u32 = AnyMetricValue; +metricInc#23e305ab = AnyMetricValue; + +metric#87d62ee3 timestamp:u64 value:AnyMetricValue metric_name:string tags:%(Span %(Pair string string)) = Metric; diff --git a/runtime-light/stdlib/diagnostics/metrics.h b/runtime-light/stdlib/diagnostics/metrics.h new file mode 100644 index 0000000000..74b9bce9ca --- /dev/null +++ b/runtime-light/stdlib/diagnostics/metrics.h @@ -0,0 +1,251 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#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" + +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); }); + } + + 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(); }); + } +}; + +struct metricCount final { + tl::u32 count{}; + + void store(tl::storer& tls) const noexcept { + count.store(tls); + } + + constexpr size_t footprint() const noexcept { + return count.footprint(); + } +}; + +struct metricInc final { + void store(tl::storer& /* tls */) const noexcept {} + + constexpr size_t footprint() const noexcept { + return 0; + } +}; + +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; + +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); + } + + std::visit([&tls](const auto& v) noexcept { v.store(tls); }, value); + } + + 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); + value.store(tls); + metric_name.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); }); + } + + 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(); }); + } +}; +} // namespace tl + +// --------------------------------------------------------------------------------------------------------- + +namespace kphp::diagnostics { +template +concept tag_range = std::ranges::range && std::is_constructible_v, std::ranges::range_value_t>; + +struct metric final { +private: + tl::storer tls; + + metric() noexcept + : tls{0} {} + + explicit metric(tl::storer&& tls) noexcept + : tls{std::move(tls)} {} + + static uint64_t ns_timestamp_now() noexcept { + k2::SystemTime st{}; + k2::system_time(std::addressof(st)); + return st.since_epoch_ns; + } + + std::expected send() const noexcept { + return k2::write_metrics(this->tls.view()); + } + + // 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)}; + } + + 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}}}; + })}; + + self.tls.reserve(serialized.footprint()); + serialized.store(self.tls); + + return std::forward(self).send(); + } + +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); + } + + 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); + } + + 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); + } + + 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); + } +}; + +// --------------------------------------------------------------------------------------------------------- + +struct metric_builder final { +private: + kphp::stl::string metric_name; + kphp::stl::vector, kphp::stl::string>, + kphp::memory::script_allocator> + tags; + + explicit metric_builder(std::string_view metric_name) noexcept + : metric_name{metric_name} {} + +public: + static metric_builder metric(std::string_view metric_name) noexcept { + return metric_builder{metric_name}; + } + + metric_builder& tag(std::string_view tag_name, std::string_view tag_value) noexcept { + this->tags.emplace_back(tag_name, tag_value); + return *this; + } + + 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); + } + + auto send_increment(std::optional timestamp = std::nullopt) const noexcept { + return metric::empty().send_increment(this->metric_name, this->tags, timestamp); + } +}; +} // namespace kphp::diagnostics diff --git a/runtime-light/tl/tl-types.h b/runtime-light/tl/tl-types.h index f3dadda7e6..53f5a56ae1 100644 --- a/runtime-light/tl/tl-types.h +++ b/runtime-light/tl/tl-types.h @@ -478,6 +478,30 @@ struct vector final { } }; +template +struct pair final { + std::pair value; + + bool fetch(tl::fetcher& tlf) noexcept + requires tl::deserializable && tl::deserializable + { + return value.first.fetch(tlf) && value.second.fetch(tlf); + } + + void store(tl::storer& tls) const noexcept + requires tl::serializable && tl::serializable + { + value.first.store(tls); + value.second.store(tls); + } + + constexpr size_t footprint() const noexcept + requires tl::footprintable && tl::footprintable + { + return value.first.footprint() + value.second.footprint(); + } +}; + template struct Vector final { tl::vector inner{};