Skip to content

Commit 9c57859

Browse files
committed
feat statistics: add MonotonicByLabelStorage
Adds @ref utils::statistics::MonotonicByLabelStorage, which is a concurrent map from labels to corresponding metrics. * It is multiple times faster compared to @ref rcu::RcuMap, which performs lots of allocations and exhibits O(N^2) complexity when adding multiple new label values. * It has a more expressive API, benefitting from C++20 struct name reflection. commit_hash:852dfa7d7a497aaeb25553e91eaa0b0baea7e4b9
1 parent 322f8ac commit 9c57859

File tree

7 files changed

+887
-0
lines changed

7 files changed

+887
-0
lines changed

.mapping.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,7 @@
12061206
"core/include/userver/utils/resource_scopes.hpp":"taxi/uservices/userver/core/include/userver/utils/resource_scopes.hpp",
12071207
"core/include/userver/utils/retry_budget.hpp":"taxi/uservices/userver/core/include/userver/utils/retry_budget.hpp",
12081208
"core/include/userver/utils/statistics/busy.hpp":"taxi/uservices/userver/core/include/userver/utils/statistics/busy.hpp",
1209+
"core/include/userver/utils/statistics/by_label_storage.hpp":"taxi/uservices/userver/core/include/userver/utils/statistics/by_label_storage.hpp",
12091210
"core/include/userver/utils/statistics/common.hpp":"taxi/uservices/userver/core/include/userver/utils/statistics/common.hpp",
12101211
"core/include/userver/utils/statistics/entry.hpp":"taxi/uservices/userver/core/include/userver/utils/statistics/entry.hpp",
12111212
"core/include/userver/utils/statistics/graphite.hpp":"taxi/uservices/userver/core/include/userver/utils/statistics/graphite.hpp",
@@ -2137,6 +2138,7 @@
21372138
"core/src/utils/signal_catcher.hpp":"taxi/uservices/userver/core/src/utils/signal_catcher.hpp",
21382139
"core/src/utils/statistics/busy.cpp":"taxi/uservices/userver/core/src/utils/statistics/busy.cpp",
21392140
"core/src/utils/statistics/busy_test.cpp":"taxi/uservices/userver/core/src/utils/statistics/busy_test.cpp",
2141+
"core/src/utils/statistics/by_label_storage_test.cpp":"taxi/uservices/userver/core/src/utils/statistics/by_label_storage_test.cpp",
21402142
"core/src/utils/statistics/common.cpp":"taxi/uservices/userver/core/src/utils/statistics/common.cpp",
21412143
"core/src/utils/statistics/entry.cpp":"taxi/uservices/userver/core/src/utils/statistics/entry.cpp",
21422144
"core/src/utils/statistics/entry_impl.hpp":"taxi/uservices/userver/core/src/utils/statistics/entry_impl.hpp",
@@ -5547,6 +5549,7 @@
55475549
"universal/include/userver/utils/projected_set.hpp":"taxi/uservices/userver/universal/include/userver/utils/projected_set.hpp",
55485550
"universal/include/userver/utils/rand.hpp":"taxi/uservices/userver/universal/include/userver/utils/rand.hpp",
55495551
"universal/include/userver/utils/regex.hpp":"taxi/uservices/userver/universal/include/userver/utils/regex.hpp",
5552+
"universal/include/userver/utils/required.hpp":"taxi/uservices/userver/universal/include/userver/utils/required.hpp",
55505553
"universal/include/userver/utils/resources.hpp":"taxi/uservices/userver/universal/include/userver/utils/resources.hpp",
55515554
"universal/include/userver/utils/result_store.hpp":"taxi/uservices/userver/universal/include/userver/utils/result_store.hpp",
55525555
"universal/include/userver/utils/scope_guard.hpp":"taxi/uservices/userver/universal/include/userver/utils/scope_guard.hpp",
@@ -5878,6 +5881,7 @@
58785881
"universal/src/utils/regex.cpp":"taxi/uservices/userver/universal/src/utils/regex.cpp",
58795882
"universal/src/utils/regex_benchmark.cpp":"taxi/uservices/userver/universal/src/utils/regex_benchmark.cpp",
58805883
"universal/src/utils/regex_test.cpp":"taxi/uservices/userver/universal/src/utils/regex_test.cpp",
5884+
"universal/src/utils/required_test.cpp":"taxi/uservices/userver/universal/src/utils/required_test.cpp",
58815885
"universal/src/utils/resources.cpp":"taxi/uservices/userver/universal/src/utils/resources.cpp",
58825886
"universal/src/utils/scope_guard_test.cpp":"taxi/uservices/userver/universal/src/utils/scope_guard_test.cpp",
58835887
"universal/src/utils/shared_readable_ptr_compilefailtest.cpp":"taxi/uservices/userver/universal/src/utils/shared_readable_ptr_compilefailtest.cpp",
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
#pragma once
2+
3+
/// @file userver/utils/statistics/by_label_storage.hpp
4+
/// @brief @copybrief utils::statistics::MonotonicByLabelStorage
5+
6+
#include <array>
7+
#include <concepts>
8+
#include <cstddef>
9+
#include <functional>
10+
#include <string>
11+
#include <string_view>
12+
13+
#include <boost/container_hash/hash.hpp>
14+
#include <boost/pfr/core.hpp>
15+
#include <boost/pfr/core_name.hpp>
16+
#include <boost/pfr/tuple_size.hpp>
17+
18+
#include <userver/concurrent/impl/monotonic_concurrent_set.hpp>
19+
#include <userver/utils/assert.hpp>
20+
#include <userver/utils/fixed_array.hpp>
21+
#include <userver/utils/statistics/labels.hpp>
22+
#include <userver/utils/statistics/writer.hpp>
23+
24+
USERVER_NAMESPACE_BEGIN
25+
26+
namespace utils::statistics {
27+
28+
namespace impl {
29+
30+
template <typename Metric>
31+
concept DumpableMetric = kHasWriterSupport<Metric>;
32+
33+
template <typename Metric>
34+
concept ResettableMetric = requires(Metric& m) { ResetMetric(m); };
35+
36+
template <typename Field>
37+
concept StringViewCompatibleField =
38+
std::constructible_from<Field, std::string_view> && std::constructible_from<std::string_view, const Field&>;
39+
40+
template <typename Labels>
41+
concept AllFieldsAreStringViewCompatible = []<std::size_t... Is>(std::index_sequence<Is...>) {
42+
return (StringViewCompatibleField<boost::pfr::tuple_element_t<Is, Labels>> && ...);
43+
}(std::make_index_sequence<boost::pfr::tuple_size_v<Labels>>{});
44+
45+
template <typename Labels>
46+
concept LabelsAggregate = std::is_aggregate_v<Labels> && AllFieldsAreStringViewCompatible<Labels>;
47+
48+
template <typename Labels>
49+
auto LabelsStructToViewArray(const Labels& labels) {
50+
constexpr std::size_t kN = boost::pfr::tuple_size_v<Labels>;
51+
std::array<std::string_view, kN> result{};
52+
boost::pfr::for_each_field(labels, [&result](const auto& field, std::size_t i) {
53+
result[i] = std::string_view{field};
54+
});
55+
return result;
56+
}
57+
58+
template <typename Labels>
59+
constexpr auto GetLabelNames() noexcept {
60+
return boost::pfr::names_as_array<Labels>();
61+
}
62+
63+
template <typename Labels, std::size_t... Is>
64+
Labels LabelsArrayToStruct(const utils::FixedArray<std::string>& arr, std::index_sequence<Is...>) {
65+
UASSERT(arr.size() == sizeof...(Is));
66+
return Labels{std::string_view{arr[Is]}...};
67+
}
68+
69+
template <typename Labels>
70+
Labels LabelsArrayToStruct(const utils::FixedArray<std::string>& arr) {
71+
return LabelsArrayToStruct<Labels>(arr, std::make_index_sequence<boost::pfr::tuple_size_v<Labels>>{});
72+
}
73+
74+
// Transparent hash for utils::FixedArray<std::string>.
75+
struct FixedStringArrayHash {
76+
using is_transparent [[maybe_unused]] = void;
77+
78+
template <typename StringViewRange>
79+
std::size_t operator()(const StringViewRange& arr) const noexcept {
80+
std::size_t hash = arr.size();
81+
for (const auto& s : arr) {
82+
boost::hash_combine(hash, std::hash<std::string_view>{}(s));
83+
}
84+
return hash;
85+
}
86+
};
87+
88+
// Transparent equality for utils::FixedArray<std::string>.
89+
struct FixedStringArrayEqual {
90+
using is_transparent [[maybe_unused]] = void;
91+
92+
template <typename StringViewRange1, typename StringViewRange2>
93+
bool operator()(const StringViewRange1& a, const StringViewRange2& b) const noexcept {
94+
UASSERT(a.size() == b.size());
95+
for (std::size_t i = 0; i < a.size(); ++i) {
96+
if (std::string_view{a[i]} != std::string_view{b[i]}) {
97+
return false;
98+
}
99+
}
100+
return true;
101+
}
102+
};
103+
104+
// Entry stored in the set: label values + metric value.
105+
template <typename Metric>
106+
struct ByLabelEntry {
107+
utils::FixedArray<std::string> labels;
108+
Metric metric;
109+
110+
template <std::size_t N, typename... Args>
111+
explicit ByLabelEntry(const std::array<std::string_view, N>& views, Args&&... args)
112+
: labels(views.begin(), views.end()),
113+
metric(std::forward<Args>(args)...)
114+
{}
115+
};
116+
117+
// Hash for ByLabelEntry - hashes only the labels part.
118+
template <typename Metric>
119+
struct ByLabelEntryHash {
120+
using is_transparent [[maybe_unused]] = void;
121+
122+
std::size_t operator()(const ByLabelEntry<Metric>& entry) const noexcept {
123+
return FixedStringArrayHash{}(entry.labels);
124+
}
125+
126+
template <std::size_t N>
127+
std::size_t operator()(const std::array<std::string_view, N>& key) const noexcept {
128+
return FixedStringArrayHash{}(key);
129+
}
130+
};
131+
132+
// Equality for ByLabelEntry - compares only the labels part.
133+
template <typename Metric>
134+
struct ByLabelEntryEqual {
135+
using is_transparent [[maybe_unused]] = void;
136+
137+
bool operator()(const ByLabelEntry<Metric>& a, const ByLabelEntry<Metric>& b) const noexcept {
138+
return FixedStringArrayEqual{}(a.labels, b.labels);
139+
}
140+
141+
template <std::size_t N>
142+
bool operator()(const ByLabelEntry<Metric>& a, const std::array<std::string_view, N>& b) const noexcept {
143+
return FixedStringArrayEqual{}(a.labels, b);
144+
}
145+
146+
template <std::size_t N>
147+
bool operator()(const std::array<std::string_view, N>& a, const ByLabelEntry<Metric>& b) const noexcept {
148+
return FixedStringArrayEqual{}(a, b.labels);
149+
}
150+
};
151+
152+
} // namespace impl
153+
154+
/// @ingroup userver_universal
155+
///
156+
/// @brief Thread-safe monotonic storage of metrics indexed by label values.
157+
///
158+
/// `Labels` must be an aggregate type where all fields are interconvertible
159+
/// with `std::string_view`, i.e. constructible from `std::string_view` and
160+
/// convertible to `std::string_view`. This includes `std::string_view` itself,
161+
/// `utils::Required<std::string_view>`, `std::string`, @ref utils::StrongTypedef, etc.
162+
///
163+
/// Label names are taken from `Labels` field names.
164+
///
165+
/// Items can only be added, never removed.
166+
///
167+
/// @warning Avoid storing high-cardinality values as labels, especially when the client input can directly
168+
/// customize label values. This can lead to security issues where clients can cause metrics quota exhaustion
169+
/// or crashes due to OOM errors.
170+
///
171+
/// ## Usage of MonotonicByLabelStorage
172+
///
173+
/// Define a labels struct with `std::string_view` fields:
174+
/// @snippet core/src/utils/statistics/by_label_storage_test.cpp by_label_storage labels struct
175+
///
176+
/// Use @ref utils::Required for fields without adequate defaults to make sure they are always provided.
177+
///
178+
/// Declare a @ref utils::statistics::MetricTag for the storage:
179+
/// @snippet core/src/utils/statistics/by_label_storage_test.cpp by_label_storage metric tag
180+
///
181+
/// Write to the storage via `Emplace`:
182+
/// @snippet core/src/utils/statistics/by_label_storage_test.cpp by_label_storage emplace
183+
///
184+
/// ## Advanced usage of MonotonicByLabelStorage
185+
///
186+
/// Any dumpable type is supported as `Metric`. When multiple metrics have the same labels,
187+
/// declare a struct with metrics and create a storage of structs instead of creating multiple storages.
188+
/// See @ref scripts/docs/en/userver/metrics.md .
189+
///
190+
/// `MonotonicByLabelStorage` is also composable the other way around, it can be included in larger metric structures
191+
/// as a field.
192+
///
193+
/// @tparam Labels An aggregate type with `std::string_view` fields.
194+
/// @tparam Metric The metric type. Must support `DumpMetric(Writer&, const Metric&)`.
195+
template <typename Labels, typename Metric>
196+
requires impl::LabelsAggregate<Labels> && impl::DumpableMetric<Metric>
197+
class MonotonicByLabelStorage final {
198+
public:
199+
/// @brief Create an empty storage.
200+
MonotonicByLabelStorage() = default;
201+
202+
MonotonicByLabelStorage(MonotonicByLabelStorage&&) = delete;
203+
MonotonicByLabelStorage& operator=(MonotonicByLabelStorage&&) = delete;
204+
205+
/// @brief Get or create a metric for the given label values.
206+
/// @param labels Label values (all fields must be `std::string_view`).
207+
/// @param args Arguments forwarded to `Metric` constructor on first insertion.
208+
/// @return Reference to the metric.
209+
template <typename... Args>
210+
requires std::constructible_from<Metric, Args...>
211+
Metric& Emplace(const Labels& labels, Args&&... args) {
212+
const auto view_array = impl::LabelsStructToViewArray(labels);
213+
auto [entry, inserted] = set_.TryEmplace(view_array, view_array, std::forward<Args>(args)...);
214+
(void)inserted;
215+
return entry.metric;
216+
}
217+
218+
/// @brief Find a metric by label values without creating it.
219+
/// @return Pointer to the metric, or nullptr if not found.
220+
Metric* GetIfExists(const Labels& labels) {
221+
const auto view_array = impl::LabelsStructToViewArray(labels);
222+
auto ref = set_.Find(view_array);
223+
if (ref) {
224+
return &ref->metric;
225+
}
226+
return nullptr;
227+
}
228+
229+
/// @brief Visit all stored metrics.
230+
/// @param func Callable accepting `(const Labels&, const Metric&)`.
231+
void VisitAll(std::invocable<const Labels&, const Metric&> auto func) const {
232+
set_.Visit([&func](const Entry& entry) { func(impl::LabelsArrayToStruct<Labels>(entry.labels), entry.metric); }
233+
);
234+
}
235+
236+
/// @brief Dump all metrics to a Writer, using label names from `Labels` fields.
237+
friend void DumpMetric(Writer& writer, const MonotonicByLabelStorage& storage) {
238+
static constexpr auto kLabelNames = impl::GetLabelNames<Labels>();
239+
static constexpr std::size_t kFieldCount = boost::pfr::tuple_size_v<Labels>;
240+
241+
std::vector<LabelView> label_views;
242+
label_views.reserve(kFieldCount);
243+
244+
storage.set_.Visit([&writer, &label_views](const Entry& entry) {
245+
label_views.clear();
246+
for (std::size_t i = 0; i < kFieldCount; ++i) {
247+
label_views.emplace_back(kLabelNames[i], entry.labels[i]);
248+
}
249+
writer.ValueWithLabels(entry.metric, label_views);
250+
});
251+
}
252+
253+
/// @brief Reset all metrics (only available if `Metric` has ADL-found `ResetMetric`).
254+
friend void ResetMetric(MonotonicByLabelStorage& storage)
255+
requires impl::ResettableMetric<Metric>
256+
{
257+
storage.set_.Visit([](Entry& entry) { ResetMetric(entry.metric); });
258+
}
259+
260+
private:
261+
using Entry = impl::ByLabelEntry<Metric>;
262+
using Set = concurrent::impl::MonotonicConcurrentSet<
263+
Entry,
264+
impl::ByLabelEntryHash<Metric>,
265+
impl::ByLabelEntryEqual<Metric>>;
266+
267+
Set set_;
268+
};
269+
270+
} // namespace utils::statistics
271+
272+
USERVER_NAMESPACE_END

0 commit comments

Comments
 (0)