|
| 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