Skip to content

Commit b36270b

Browse files
authored
[k2] add metrics builder and k2_write_metric API (#1639)
1 parent f47b16f commit b36270b

5 files changed

Lines changed: 321 additions & 0 deletions

File tree

runtime-light/k2-platform/k2-api.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,13 @@ inline std::expected<void, int32_t> madvise(void* addr, size_t length, int32_t a
245245
return {};
246246
}
247247

248+
inline std::expected<void, int32_t> write_metrics(std::span<const std::byte> serialized_metric) noexcept {
249+
if (auto error_code{k2_write_metrics(serialized_metric.data(), serialized_metric.size())}; error_code != k2::errno_ok) [[unlikely]] {
250+
return std::unexpected{error_code};
251+
}
252+
return {};
253+
}
254+
248255
inline void please_shutdown(k2::descriptor descriptor) noexcept {
249256
k2_please_shutdown(descriptor);
250257
}

runtime-light/k2-platform/k2-header.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,32 @@ void* k2_mmap(uint64_t* md, void* addr, size_t length, int32_t prot, int32_t fla
383383
*/
384384
int32_t k2_madvise(void* addr, size_t length, int32_t advise);
385385

386+
/**
387+
* Writes a pre-serialized metrics to the specified monitoring system.
388+
*
389+
* The buffer must contain a metric serialized according to the following format
390+
* (TL serialization, native byte order):
391+
* <timestamp:u64><metric name:tl string><value format><tags count:u32><tag1><tag2>...
392+
* tag := <name:tl string><value:tl string>
393+
*
394+
* value format:
395+
* <`VALUE_MAGIC`:u32><f64> - single double value
396+
* <`VALUES_ARRAY_MAGIC`:u32><len:u32><f64><f64>... - array of double values
397+
* <`COUNT_MAGIC`:u32><u32> - count value
398+
* <`INC_MAGIC`:u32> - counter increment
399+
*
400+
* tl string is the standard TL string encoding.
401+
*
402+
* Multiple metrics can be sent in a single call by concatenating them sequentially:
403+
* <metric1><metric2>...
404+
* Each metric is serialized independently using the format described above.
405+
*
406+
* @param `buf` A pointer to the serialized metric(s) data.
407+
* @param `buf_len` The length of the serialized metric(s) data in bytes.
408+
* @return returns 0 if everything is fine, otherwise error code
409+
*/
410+
int32_t k2_write_metrics(const void* buf, size_t buf_len);
411+
386412
/**
387413
* Sets `StreamStatus.please_whutdown_write=true` for the component on the
388414
* opposite side (does not affect `StreamStatus` on your side).
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---types---
2+
3+
pair {X:Type} {Y:Type} a:X b:Y = Pair X Y;
4+
span#dad3ae87 {t:Type} count:# data:count*[t] = Span t;
5+
6+
// Metric
7+
8+
metricValue#0cb5eb87 value:double = AnyMetricValue;
9+
metricValuesArray#d4a59582 values:%(Span double) = AnyMetricValue;
10+
metricCount#941bf7d1 count:u32 = AnyMetricValue;
11+
metricInc#23e305ab = AnyMetricValue;
12+
13+
metric#87d62ee3 timestamp:u64 value:AnyMetricValue metric_name:string tags:%(Span %(Pair string string)) = Metric;
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// Compiler for PHP (aka KPHP)
2+
// Copyright (c) 2026 LLC «V Kontakte»
3+
// Distributed under the GPL v3 License, see LICENSE.notice.txt
4+
5+
#pragma once
6+
7+
#include <cstddef>
8+
#include <cstdint>
9+
#include <expected>
10+
#include <memory>
11+
#include <optional>
12+
#include <ranges>
13+
#include <span>
14+
#include <string_view>
15+
#include <type_traits>
16+
#include <utility>
17+
#include <variant>
18+
19+
#include "runtime-common/core/allocator/script-allocator.h"
20+
#include "runtime-common/core/std/containers.h"
21+
#include "runtime-light/k2-platform/k2-api.h"
22+
#include "runtime-light/tl/tl-core.h"
23+
#include "runtime-light/tl/tl-types.h"
24+
25+
namespace tl {
26+
struct metricValue final {
27+
tl::f64 value{};
28+
29+
void store(tl::storer& tls) const noexcept {
30+
value.store(tls);
31+
}
32+
33+
constexpr size_t footprint() const noexcept {
34+
return value.footprint();
35+
}
36+
};
37+
38+
struct metricValuesArray final {
39+
std::span<const double> values;
40+
41+
void store(tl::storer& tls) const noexcept {
42+
tl::u32{static_cast<uint32_t>(this->values.size())}.store(tls);
43+
std::ranges::for_each(this->values, [&tls](const double& elem) noexcept { tl::f64{elem}.store(tls); });
44+
}
45+
46+
constexpr size_t footprint() const noexcept {
47+
return std::ranges::fold_left(this->values, tl::u32{static_cast<uint32_t>(this->values.size())}.footprint(),
48+
[](size_t acc, const double& elem) noexcept { return acc + tl::f64{elem}.footprint(); });
49+
}
50+
};
51+
52+
struct metricCount final {
53+
tl::u32 count{};
54+
55+
void store(tl::storer& tls) const noexcept {
56+
count.store(tls);
57+
}
58+
59+
constexpr size_t footprint() const noexcept {
60+
return count.footprint();
61+
}
62+
};
63+
64+
struct metricInc final {
65+
void store(tl::storer& /* tls */) const noexcept {}
66+
67+
constexpr size_t footprint() const noexcept {
68+
return 0;
69+
}
70+
};
71+
72+
class AnyMetricValue final {
73+
static constexpr uint32_t VALUE_MAGIC = 0xcb5eb87U;
74+
static constexpr uint32_t VALUES_ARRAY_MAGIC = 0xd4a59582U;
75+
static constexpr uint32_t COUNT_MAGIC = 0x941bf7d1U;
76+
static constexpr uint32_t INC_MAGIC = 0x23e305abU;
77+
78+
public:
79+
std::variant<metricValue, metricValuesArray, metricCount, metricInc> value;
80+
81+
void store(tl::storer& tls) const noexcept {
82+
if (std::holds_alternative<metricValue>(value)) {
83+
tl::magic{.value = AnyMetricValue::VALUE_MAGIC}.store(tls);
84+
} else if (std::holds_alternative<metricValuesArray>(value)) {
85+
tl::magic{.value = AnyMetricValue::VALUES_ARRAY_MAGIC}.store(tls);
86+
} else if (std::holds_alternative<metricCount>(value)) {
87+
tl::magic{.value = AnyMetricValue::COUNT_MAGIC}.store(tls);
88+
} else {
89+
tl::magic{.value = AnyMetricValue::INC_MAGIC}.store(tls);
90+
}
91+
92+
std::visit([&tls](const auto& v) noexcept { v.store(tls); }, value);
93+
}
94+
95+
constexpr size_t footprint() const noexcept {
96+
return tl::magic{}.footprint() + std::visit([](const auto& v) noexcept { return v.footprint(); }, value);
97+
}
98+
};
99+
100+
template<std::ranges::range TagRange>
101+
requires std::same_as<std::remove_cvref_t<std::ranges::range_value_t<TagRange>>, tl::pair<tl::string, tl::string>>
102+
struct metric final {
103+
tl::u64 timestamp{};
104+
tl::AnyMetricValue value{};
105+
tl::string metric_name{};
106+
TagRange tags{};
107+
108+
void store(tl::storer& tls) const noexcept
109+
requires tl::serializable<std::ranges::range_value_t<TagRange>>
110+
{
111+
timestamp.store(tls);
112+
metric_name.store(tls);
113+
value.store(tls);
114+
115+
tl::u32{.value = static_cast<uint32_t>(std::ranges::distance(tags))}.store(tls);
116+
std::ranges::for_each(tags, [&tls](const auto& elem) noexcept { elem.store(tls); });
117+
}
118+
119+
constexpr size_t footprint() const noexcept
120+
requires tl::footprintable<std::ranges::range_value_t<TagRange>>
121+
{
122+
return timestamp.footprint() + value.footprint() + metric_name.footprint() +
123+
std::ranges::fold_left(tags, tl::u32{.value = static_cast<uint32_t>(std::ranges::distance(tags))}.footprint(),
124+
[](size_t acc, const auto& elem) noexcept { return acc + elem.footprint(); });
125+
}
126+
};
127+
} // namespace tl
128+
129+
// ---------------------------------------------------------------------------------------------------------
130+
131+
namespace kphp::diagnostics {
132+
template<typename T>
133+
concept tag_range = std::ranges::range<T> && std::is_constructible_v<std::pair<std::string_view, std::string_view>, std::ranges::range_value_t<T>>;
134+
135+
struct metric final {
136+
private:
137+
tl::storer tls;
138+
139+
metric() noexcept
140+
: tls{0} {}
141+
142+
explicit metric(tl::storer&& tls) noexcept
143+
: tls{std::move(tls)} {}
144+
145+
static uint64_t ns_timestamp_now() noexcept {
146+
k2::SystemTime st{};
147+
k2::system_time(std::addressof(st));
148+
return st.since_epoch_ns;
149+
}
150+
151+
std::expected<void, int32_t> send() const noexcept {
152+
return k2::write_metrics(this->tls.view());
153+
}
154+
155+
// clears buffer and returns it with preserved capacity for reuse by metric::with_buffer()
156+
std::pair<tl::storer, std::expected<void, int32_t>> send() && noexcept {
157+
std::expected<void, int32_t> send_result{k2::write_metrics(this->tls.view())};
158+
this->tls.clear();
159+
return std::pair{std::move(this->tls), std::move(send_result)};
160+
}
161+
162+
template<typename Self, tag_range TagRange>
163+
decltype(auto) build_and_send(this Self&& self, std::string_view metric_name, TagRange&& tags, tl::AnyMetricValue value,
164+
std::optional<uint64_t> timestamp) noexcept {
165+
self.tls.clear();
166+
167+
uint64_t ns_timestamp{timestamp.value_or(metric::ns_timestamp_now())};
168+
tl::metric serialized{.timestamp = tl::u64{ns_timestamp},
169+
.value = value,
170+
.metric_name = tl::string{metric_name},
171+
.tags = std::forward<TagRange>(tags) | std::views::transform([](const auto& elem) noexcept -> tl::pair<tl::string, tl::string> {
172+
std::pair<std::string_view, std::string_view> sv_pair{elem};
173+
return tl::pair{std::pair{tl::string{sv_pair.first}, tl::string{sv_pair.second}}};
174+
})};
175+
176+
self.tls.reserve(serialized.footprint());
177+
serialized.store(self.tls);
178+
179+
return std::forward<Self>(self).send();
180+
}
181+
182+
public:
183+
static metric empty() noexcept {
184+
return metric{};
185+
}
186+
187+
static metric with_buffer(tl::storer&& tls) noexcept {
188+
return metric{std::move(tls)};
189+
}
190+
191+
template<typename Self, tag_range TagRange>
192+
auto send_value(this Self&& self, std::string_view metric_name, TagRange&& tags, double value, std::optional<uint64_t> timestamp = std::nullopt) noexcept {
193+
return std::forward<Self>(self).build_and_send(metric_name, std::forward<TagRange>(tags), tl::AnyMetricValue{tl::metricValue{tl::f64{value}}}, timestamp);
194+
}
195+
196+
template<typename Self, tag_range TagRange>
197+
auto send_values_array(this Self&& self, std::string_view metric_name, TagRange&& tags, std::span<const double> values,
198+
std::optional<uint64_t> timestamp = std::nullopt) noexcept {
199+
return std::forward<Self>(self).build_and_send(metric_name, std::forward<TagRange>(tags), tl::AnyMetricValue{tl::metricValuesArray{values}}, timestamp);
200+
}
201+
202+
template<typename Self, tag_range TagRange>
203+
auto send_count(this Self&& self, std::string_view metric_name, TagRange&& tags, uint32_t count, std::optional<uint64_t> timestamp = std::nullopt) noexcept {
204+
return std::forward<Self>(self).build_and_send(metric_name, std::forward<TagRange>(tags), tl::AnyMetricValue{tl::metricCount{tl::u32{count}}}, timestamp);
205+
}
206+
207+
template<typename Self, tag_range TagRange>
208+
auto send_increment(this Self&& self, std::string_view metric_name, TagRange&& tags, std::optional<uint64_t> timestamp = std::nullopt) noexcept {
209+
return std::forward<Self>(self).build_and_send(metric_name, std::forward<TagRange>(tags), tl::AnyMetricValue{tl::metricInc{}}, timestamp);
210+
}
211+
};
212+
213+
// ---------------------------------------------------------------------------------------------------------
214+
215+
struct metric_builder final {
216+
private:
217+
kphp::stl::string<kphp::memory::script_allocator> metric_name;
218+
kphp::stl::vector<std::pair<kphp::stl::string<kphp::memory::script_allocator>, kphp::stl::string<kphp::memory::script_allocator>>,
219+
kphp::memory::script_allocator>
220+
tags;
221+
222+
explicit metric_builder(std::string_view metric_name) noexcept
223+
: metric_name{metric_name} {}
224+
225+
public:
226+
static metric_builder metric(std::string_view metric_name) noexcept {
227+
return metric_builder{metric_name};
228+
}
229+
230+
metric_builder& tag(std::string_view tag_name, std::string_view tag_value) noexcept {
231+
this->tags.emplace_back(tag_name, tag_value);
232+
return *this;
233+
}
234+
235+
auto send_value(double value, std::optional<uint64_t> timestamp = std::nullopt) const noexcept {
236+
return metric::empty().send_value(this->metric_name, this->tags, value, timestamp);
237+
}
238+
239+
auto send_values_array(std::span<const double> values, std::optional<uint64_t> timestamp = std::nullopt) const noexcept {
240+
return metric::empty().send_values_array(this->metric_name, this->tags, values, timestamp);
241+
}
242+
243+
auto send_count(uint32_t count, std::optional<uint64_t> timestamp = std::nullopt) const noexcept {
244+
return metric::empty().send_count(this->metric_name, this->tags, count, timestamp);
245+
}
246+
247+
auto send_increment(std::optional<uint64_t> timestamp = std::nullopt) const noexcept {
248+
return metric::empty().send_increment(this->metric_name, this->tags, timestamp);
249+
}
250+
};
251+
} // namespace kphp::diagnostics

runtime-light/tl/tl-types.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,30 @@ struct vector final {
478478
}
479479
};
480480

481+
template<typename F, typename S>
482+
struct pair final {
483+
std::pair<F, S> value;
484+
485+
bool fetch(tl::fetcher& tlf) noexcept
486+
requires tl::deserializable<F> && tl::deserializable<S>
487+
{
488+
return value.first.fetch(tlf) && value.second.fetch(tlf);
489+
}
490+
491+
void store(tl::storer& tls) const noexcept
492+
requires tl::serializable<F> && tl::serializable<S>
493+
{
494+
value.first.store(tls);
495+
value.second.store(tls);
496+
}
497+
498+
constexpr size_t footprint() const noexcept
499+
requires tl::footprintable<F> && tl::footprintable<S>
500+
{
501+
return value.first.footprint() + value.second.footprint();
502+
}
503+
};
504+
481505
template<typename T>
482506
struct Vector final {
483507
tl::vector<T> inner{};

0 commit comments

Comments
 (0)