userver: userver/utils/statistics/by_label_storage.hpp Source File
Loading...
Searching...
No Matches
by_label_storage.hpp
Go to the documentation of this file.
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
24USERVER_NAMESPACE_BEGIN
25
26namespace utils::statistics {
27
28namespace impl {
29
30template <typename Metric>
31concept DumpableMetric = kHasWriterSupport<Metric>;
32
33template <typename Metric>
34concept ResettableMetric = requires(Metric& m) { ResetMetric(m); };
35
36template <typename Field>
37std::string_view FieldToStringView(const Field& field) {
38 if constexpr (std::constructible_from<std::string_view, const Field&>) {
39 return std::string_view{field};
40 } else {
41 // ADL lookup.
42 return ToStringView(field);
43 }
44}
45
46template <typename Field>
47concept FieldConvertibleToStringView =
48 std::constructible_from<std::string_view, const Field&> || requires(const Field& f) {
49 {
50 ToStringView(f)
51 } -> std::same_as<std::string_view>;
52 };
53
54template <typename Field>
55concept FieldConstructibleFromStringView = std::constructible_from<Field, std::string_view>;
56
57template <typename Labels>
58concept AllFieldsConvertibleToStringView = []<std::size_t... Is>(std::index_sequence<Is...>) {
59 return (FieldConvertibleToStringView<boost::pfr::tuple_element_t<Is, Labels>> && ...);
60}(std::make_index_sequence<boost::pfr::tuple_size_v<Labels>>{});
61
62template <typename Labels>
63concept AllFieldsConstructibleFromStringView = []<std::size_t... Is>(std::index_sequence<Is...>) {
64 return (FieldConstructibleFromStringView<boost::pfr::tuple_element_t<Is, Labels>> && ...);
65}(std::make_index_sequence<boost::pfr::tuple_size_v<Labels>>{});
66
67template <typename Labels>
68concept LabelsAggregate = std::is_aggregate_v<Labels> && AllFieldsConvertibleToStringView<Labels>;
69
70template <typename Labels>
71auto LabelsStructToViewArray(const Labels& labels) {
72 constexpr std::size_t kN = boost::pfr::tuple_size_v<Labels>;
73 std::array<std::string_view, kN> result{};
74 boost::pfr::for_each_field(labels, [&result](const auto& field, std::size_t i) {
75 result[i] = FieldToStringView(field);
76 });
77 return result;
78}
79
80template <typename Labels>
81constexpr auto GetLabelNames() noexcept {
82 return boost::pfr::names_as_array<Labels>();
83}
84
85template <typename Labels, std::size_t... Is>
86Labels LabelsArrayToStruct(const utils::FixedArray<std::string>& arr, std::index_sequence<Is...>) {
87 UASSERT(arr.size() == sizeof...(Is));
88 return Labels{std::string_view{arr[Is]}...};
89}
90
91template <typename Labels>
92Labels LabelsArrayToStruct(const utils::FixedArray<std::string>& arr) {
93 return LabelsArrayToStruct<Labels>(arr, std::make_index_sequence<boost::pfr::tuple_size_v<Labels>>{});
94}
95
96// Transparent hash for utils::FixedArray<std::string>.
97struct FixedStringArrayHash {
98 using is_transparent [[maybe_unused]] = void;
99
100 template <typename StringViewRange>
101 std::size_t operator()(const StringViewRange& arr) const noexcept {
102 std::size_t hash = arr.size();
103 for (const auto& s : arr) {
104 boost::hash_combine(hash, std::hash<std::string_view>{}(s));
105 }
106 return hash;
107 }
108};
109
110// Transparent equality for utils::FixedArray<std::string>.
111struct FixedStringArrayEqual {
112 using is_transparent [[maybe_unused]] = void;
113
114 template <typename StringViewRange1, typename StringViewRange2>
115 bool operator()(const StringViewRange1& a, const StringViewRange2& b) const noexcept {
116 UASSERT(a.size() == b.size());
117 for (std::size_t i = 0; i < a.size(); ++i) {
118 if (std::string_view{a[i]} != std::string_view{b[i]}) {
119 return false;
120 }
121 }
122 return true;
123 }
124};
125
126// Entry stored in the set: label values + metric value.
127template <typename Metric>
128struct ByLabelEntry {
129 utils::FixedArray<std::string> labels;
130 Metric metric;
131
132 template <std::size_t N, typename... Args>
133 explicit ByLabelEntry(const std::array<std::string_view, N>& views, Args&&... args)
134 : labels(views.begin(), views.end()),
135 metric(std::forward<Args>(args)...)
136 {}
137};
138
139// Hash for ByLabelEntry - hashes only the labels part.
140template <typename Metric>
141struct ByLabelEntryHash {
142 using is_transparent [[maybe_unused]] = void;
143
144 std::size_t operator()(const ByLabelEntry<Metric>& entry) const noexcept {
145 return FixedStringArrayHash{}(entry.labels);
146 }
147
148 template <std::size_t N>
149 std::size_t operator()(const std::array<std::string_view, N>& key) const noexcept {
150 return FixedStringArrayHash{}(key);
151 }
152};
153
154// Equality for ByLabelEntry - compares only the labels part.
155template <typename Metric>
156struct ByLabelEntryEqual {
157 using is_transparent [[maybe_unused]] = void;
158
159 bool operator()(const ByLabelEntry<Metric>& a, const ByLabelEntry<Metric>& b) const noexcept {
160 return FixedStringArrayEqual{}(a.labels, b.labels);
161 }
162
163 template <std::size_t N>
164 bool operator()(const ByLabelEntry<Metric>& a, const std::array<std::string_view, N>& b) const noexcept {
165 return FixedStringArrayEqual{}(a.labels, b);
166 }
167
168 template <std::size_t N>
169 bool operator()(const std::array<std::string_view, N>& a, const ByLabelEntry<Metric>& b) const noexcept {
170 return FixedStringArrayEqual{}(a, b.labels);
171 }
172};
173
174} // namespace impl
175
176/// @ingroup userver_universal
177///
178/// @brief Thread-safe monotonic storage of metrics indexed by label values.
179///
180/// See @ref scripts/docs/en/userver/metrics.md .
181///
182/// `Labels` must be an aggregate type where all fields are convertible to `std::string_view`, by at least one of:
183///
184/// * a (possibly `explicit`) conversion operator to `std::string_view`;
185/// * an ADL-found `std::string_view ToStringView(const Field&)` function.
186///
187/// This includes `std::string_view` itself, `utils::Required<std::string_view>`,
188/// `std::string`, @ref utils::StrongTypedef, code-generated enums, etc.
189///
190/// Label names are taken from `Labels` field names.
191///
192/// Items can only be added, never removed.
193///
194/// @warning Avoid storing high-cardinality values as labels, especially when the client input can directly
195/// customize label values. This can lead to security issues where clients can cause metrics quota exhaustion
196/// or crashes due to OOM errors.
197///
198/// ## Usage of MonotonicByLabelStorage
199///
200/// Define a labels struct with `std::string_view` fields:
201/// @snippet core/src/utils/statistics/by_label_storage_test.cpp by_label_storage labels struct
202///
203/// Use @ref utils::Required for fields without adequate defaults to make sure they are always provided.
204///
205/// Declare a @ref utils::statistics::MetricTag for the storage:
206/// @snippet core/src/utils/statistics/by_label_storage_test.cpp by_label_storage metric tag
207///
208/// Write to the storage via `Emplace`:
209/// @snippet core/src/utils/statistics/by_label_storage_test.cpp by_label_storage emplace
210///
211/// ## Usage of MonotonicByLabelStorage with a group of per-label metrics
212///
213/// Any dumpable type is supported as `Metric`. When multiple metrics have the same labels,
214/// declare a struct with metrics and create a storage of structs instead of creating multiple storages.
215///
216/// Define the metric struct and its `DumpMetric`:
217/// @snippet core/src/utils/statistics/by_label_storage_test.cpp composite metric structs
218///
219/// Define the labels struct:
220/// @snippet core/src/utils/statistics/by_label_storage_test.cpp composite metric labels
221///
222/// Declare a @ref utils::statistics::MetricTag for the storage:
223/// @snippet core/src/utils/statistics/by_label_storage_test.cpp composite metric tag
224///
225/// Write to the storage:
226/// @snippet core/src/utils/statistics/by_label_storage_test.cpp composite metric usage
227///
228/// `MonotonicByLabelStorage` is also composable the other way around, it can be included in larger metric structures
229/// as a field.
230///
231/// ## Usage of MonotonicByLabelStorage with enum labels
232///
233/// An enum can be used as a label as long as it has `ToStringView` defined. Usage example:
234///
235/// @snippet core/src/utils/statistics/by_label_storage_test.cpp enum label ToStringView
236///
237/// @snippet core/src/utils/statistics/by_label_storage_test.cpp enum label labels
238///
239/// @snippet core/src/utils/statistics/by_label_storage_test.cpp enum label metric tag
240///
241/// @snippet core/src/utils/statistics/by_label_storage_test.cpp enum label usage
242///
243/// @tparam Labels An aggregate type whose fields are convertible to `std::string_view`.
244/// @tparam Metric The metric type. Must support `DumpMetric(Writer&, const Metric&)`.
245template <typename Labels, typename Metric>
246requires impl::LabelsAggregate<Labels>
248public:
249 /// @brief Create an empty storage.
251
252 MonotonicByLabelStorage(MonotonicByLabelStorage&&) = delete;
254
255 /// @brief Get or create a default-constructed metric for the given label values.
256 /// @param labels Label values.
257 /// @return Reference to the metric, valid until the storage's destruction.
258 Metric& operator[](const Labels& labels)
259 requires std::default_initializable<Metric>
260 {
261 return Emplace(labels);
262 }
263
264 /// @brief Get or create a metric for the given label values.
265 /// @param labels Label values.
266 /// @param args Arguments forwarded to `Metric` constructor on first insertion.
267 /// @return Reference to the metric, valid until the storage's destruction.
268 template <typename... Args>
269 requires std::constructible_from<Metric, Args...>
270 Metric& Emplace(const Labels& labels, Args&&... args) {
271 const auto view_array = impl::LabelsStructToViewArray(labels);
272 auto [entry, inserted] = set_.TryEmplace(view_array, view_array, std::forward<Args>(args)...);
273 (void)inserted;
274 return entry.metric;
275 }
276
277 /// @brief Find a metric by label values without creating it.
278 /// @return Pointer to the metric (valid until the storage's destruction), or nullptr if not found.
279 Metric* GetIfExists(const Labels& labels) {
280 const auto view_array = impl::LabelsStructToViewArray(labels);
281 auto ref = set_.Find(view_array);
282 if (ref) {
283 return &ref->metric;
284 }
285 return nullptr;
286 }
287
288 /// @brief Visit all stored metrics.
289 ///
290 /// Requires that all fields of `Labels` are constructible from `std::string_view`.
291 ///
292 /// @param func Callable accepting `(const Labels&, const Metric&)`.
293 void VisitAll(std::invocable<const Labels&, const Metric&> auto func) const
295 {
297 );
298 }
299
300 /// @brief Dump all metrics to a Writer, using label names from `Labels` fields.
301 friend void DumpMetric(Writer& writer, const MonotonicByLabelStorage& storage)
302 requires impl::DumpableMetric<Metric>
303 {
304 static constexpr auto kLabelNames = impl::GetLabelNames<Labels>();
305 static constexpr std::size_t kFieldCount = boost::pfr::tuple_size_v<Labels>;
306
307 std::vector<LabelView> label_views;
308 label_views.reserve(kFieldCount);
309
310 storage.set_.Visit([&writer, &label_views](const Entry& entry) {
311 label_views.clear();
312 for (std::size_t i = 0; i < kFieldCount; ++i) {
313 label_views.emplace_back(kLabelNames[i], entry.labels[i]);
314 }
315 writer.ValueWithLabels(entry.metric, label_views);
316 });
317 }
318
319 /// @brief Reset all metrics (only available if `Metric` has ADL-found `ResetMetric`).
321 requires impl::ResettableMetric<Metric>
322 {
323 storage.set_.Visit([](Entry& entry) { ResetMetric(entry.metric); });
324 }
325
326private:
327 using Entry = impl::ByLabelEntry<Metric>;
328 using Set = concurrent::impl::MonotonicConcurrentSet<
329 Entry,
330 impl::ByLabelEntryHash<Metric>,
331 impl::ByLabelEntryEqual<Metric>>;
332
333 Set set_;
334};
335
336} // namespace utils::statistics
337
338USERVER_NAMESPACE_END