Accept initializer_list, span, and string_view in api

This commit is contained in:
2025-08-31 12:31:29 -04:00
parent 93ccd2eb71
commit 4b2c5b8ce8
2 changed files with 89 additions and 83 deletions

View File

@@ -56,22 +56,22 @@ namespace metric {
// Validation helper that works in both debug and release builds // Validation helper that works in both debug and release builds
static void validate_or_abort(bool condition, const char *message, static void validate_or_abort(bool condition, const char *message,
const char *value) { std::string_view value) {
if (!condition) { if (!condition) {
std::fprintf(stderr, "WeaselDB metric validation failed: %s: '%s'\n", std::fprintf(stderr, "WeaselDB metric validation failed: %s: '%.*s'\n",
message, value); message, static_cast<int>(value.size()), value.data());
std::abort(); std::abort();
} }
} }
// Helper to copy a string into arena memory // Helper to copy a string into arena memory
static std::string_view arena_copy_string(const std::string &str, static std::string_view arena_copy_string(std::string_view str,
ArenaAllocator &arena) { ArenaAllocator &arena) {
if (str.empty()) { if (str.empty()) {
return std::string_view{}; return std::string_view{};
} }
char *copied = arena.allocate<char>(str.size() + 1); char *copied = arena.allocate<char>(str.size() + 1);
std::memcpy(copied, str.c_str(), str.size()); std::memcpy(copied, str.data(), str.size());
copied[str.size()] = '\0'; copied[str.size()] = '\0';
return std::string_view(copied, str.size()); return std::string_view(copied, str.size());
} }
@@ -81,15 +81,14 @@ static std::string_view arena_copy_string(const std::string &str,
struct LabelsKey { struct LabelsKey {
ArenaVector<std::pair<std::string_view, std::string_view>> labels; ArenaVector<std::pair<std::string_view, std::string_view>> labels;
LabelsKey(const std::vector<std::pair<std::string, std::string>> &l, LabelsKey(std::span<const std::pair<std::string_view, std::string_view>> l,
ArenaAllocator &arena) ArenaAllocator &arena)
: labels(&arena) { : labels(&arena) {
// Copy and validate all label keys and values into arena // Copy and validate all label keys and values into arena
for (const auto &[key, value] : l) { for (const auto &[key, value] : l) {
validate_or_abort(is_valid_label_key(key), "invalid label key", validate_or_abort(is_valid_label_key(key), "invalid label key", key);
key.c_str());
validate_or_abort(is_valid_label_value(value), "invalid label value", validate_or_abort(is_valid_label_value(value), "invalid label value",
value.c_str()); value);
auto key_view = arena_copy_string(key, arena); auto key_view = arena_copy_string(key, arena);
auto value_view = arena_copy_string(value, arena); auto value_view = arena_copy_string(value, arena);
@@ -382,19 +381,18 @@ struct Metric {
static Counter create_counter_instance( static Counter create_counter_instance(
Family<Counter> *family, Family<Counter> *family,
const std::vector<std::pair<std::string, std::string>> &labels) { std::span<const std::pair<std::string_view, std::string_view>> labels) {
// Force thread_local initialization // Force thread_local initialization
(void)thread_init; (void)thread_init;
std::unique_lock<std::mutex> _{mutex}; std::unique_lock<std::mutex> _{mutex};
LabelsKey key{labels, get_thread_local_arena()}; LabelsKey key{labels, get_global_arena()};
// Validate that labels aren't already registered as callback // Validate that labels aren't already registered as callback
validate_or_abort( validate_or_abort(family->p->callbacks.find(key) ==
family->p->callbacks.find(key) == family->p->callbacks.end(), family->p->callbacks.end(),
"labels already registered as callback", "labels already registered as callback",
key.labels.empty() ? "(no labels)" key.labels.empty() ? "(no labels)" : key.labels[0].first);
: std::string(key.labels[0].first).c_str());
// Ensure thread state exists // Ensure thread state exists
auto thread_id = std::this_thread::get_id(); auto thread_id = std::this_thread::get_id();
@@ -419,16 +417,15 @@ struct Metric {
static Gauge create_gauge_instance( static Gauge create_gauge_instance(
Family<Gauge> *family, Family<Gauge> *family,
const std::vector<std::pair<std::string, std::string>> &labels) { std::span<const std::pair<std::string_view, std::string_view>> labels) {
std::unique_lock<std::mutex> _{mutex}; std::unique_lock<std::mutex> _{mutex};
LabelsKey key{labels, get_global_arena()}; LabelsKey key{labels, get_global_arena()};
// Validate that labels aren't already registered as callback // Validate that labels aren't already registered as callback
validate_or_abort( validate_or_abort(family->p->callbacks.find(key) ==
family->p->callbacks.find(key) == family->p->callbacks.end(), family->p->callbacks.end(),
"labels already registered as callback", "labels already registered as callback",
key.labels.empty() ? "(no labels)" key.labels.empty() ? "(no labels)" : key.labels[0].first);
: std::string(key.labels[0].first).c_str());
auto &ptr = family->p->instances[key]; auto &ptr = family->p->instances[key];
if (!ptr) { if (!ptr) {
@@ -442,12 +439,12 @@ struct Metric {
static Histogram create_histogram_instance( static Histogram create_histogram_instance(
Family<Histogram> *family, Family<Histogram> *family,
const std::vector<std::pair<std::string, std::string>> &labels) { std::span<const std::pair<std::string_view, std::string_view>> labels) {
// Force thread_local initialization // Force thread_local initialization
(void)thread_init; (void)thread_init;
std::unique_lock<std::mutex> _{mutex}; std::unique_lock<std::mutex> _{mutex};
LabelsKey key{labels, get_thread_local_arena()}; LabelsKey key{labels, get_global_arena()};
// Ensure thread state exists // Ensure thread state exists
auto thread_id = std::this_thread::get_id(); auto thread_id = std::this_thread::get_id();
@@ -512,7 +509,7 @@ void Counter::inc(double x) {
// Validate monotonic property (counter never decreases) // Validate monotonic property (counter never decreases)
if (new_value < p->value) [[unlikely]] { if (new_value < p->value) [[unlikely]] {
validate_or_abort(false, "counter value overflow/wraparound detected", validate_or_abort(false, "counter value overflow/wraparound detected",
std::to_string(new_value).c_str()); std::to_string(new_value));
} }
__atomic_store(&p->value, &new_value, __ATOMIC_RELAXED); __atomic_store(&p->value, &new_value, __ATOMIC_RELAXED);
@@ -605,25 +602,24 @@ template <> Family<Histogram>::Family() = default;
template <> template <>
Counter Family<Counter>::create( Counter Family<Counter>::create(
std::vector<std::pair<std::string, std::string>> labels) { std::span<const std::pair<std::string_view, std::string_view>> labels) {
return Metric::create_counter_instance(this, labels); return Metric::create_counter_instance(this, labels);
} }
template <> template <>
Gauge Family<Gauge>::create( Gauge Family<Gauge>::create(
std::vector<std::pair<std::string, std::string>> labels) { std::span<const std::pair<std::string_view, std::string_view>> labels) {
return Metric::create_gauge_instance(this, labels); return Metric::create_gauge_instance(this, labels);
} }
template <> template <>
Histogram Family<Histogram>::create( Histogram Family<Histogram>::create(
std::vector<std::pair<std::string, std::string>> labels) { std::span<const std::pair<std::string_view, std::string_view>> labels) {
return Metric::create_histogram_instance(this, labels); return Metric::create_histogram_instance(this, labels);
} }
Family<Counter> create_counter(std::string name, std::string help) { Family<Counter> create_counter(std::string_view name, std::string_view help) {
validate_or_abort(is_valid_metric_name(name), "invalid counter name", validate_or_abort(is_valid_metric_name(name), "invalid counter name", name);
name.c_str());
std::unique_lock<std::mutex> _{Metric::mutex}; std::unique_lock<std::mutex> _{Metric::mutex};
auto &global_arena = Metric::get_global_arena(); auto &global_arena = Metric::get_global_arena();
@@ -640,17 +636,15 @@ Family<Counter> create_counter(std::string name, std::string help) {
} else { } else {
validate_or_abort( validate_or_abort(
familyPtr->help == help, familyPtr->help == help,
"metric family already registered with different help text", "metric family already registered with different help text", name);
name.c_str());
} }
Family<Counter> family; Family<Counter> family;
family.p = familyPtr; family.p = familyPtr;
return family; return family;
} }
Family<Gauge> create_gauge(std::string name, std::string help) { Family<Gauge> create_gauge(std::string_view name, std::string_view help) {
validate_or_abort(is_valid_metric_name(name), "invalid gauge name", validate_or_abort(is_valid_metric_name(name), "invalid gauge name", name);
name.c_str());
std::unique_lock<std::mutex> _{Metric::mutex}; std::unique_lock<std::mutex> _{Metric::mutex};
auto &global_arena = Metric::get_global_arena(); auto &global_arena = Metric::get_global_arena();
@@ -667,18 +661,16 @@ Family<Gauge> create_gauge(std::string name, std::string help) {
} else { } else {
validate_or_abort( validate_or_abort(
familyPtr->help == help, familyPtr->help == help,
"metric family already registered with different help text", "metric family already registered with different help text", name);
name.c_str());
} }
Family<Gauge> family; Family<Gauge> family;
family.p = familyPtr; family.p = familyPtr;
return family; return family;
} }
Family<Histogram> create_histogram(std::string name, std::string help, Family<Histogram> create_histogram(std::string_view name, std::string_view help,
std::span<const double> buckets) { std::span<const double> buckets) {
validate_or_abort(is_valid_metric_name(name), "invalid histogram name", validate_or_abort(is_valid_metric_name(name), "invalid histogram name", name);
name.c_str());
std::unique_lock<std::mutex> _{Metric::mutex}; std::unique_lock<std::mutex> _{Metric::mutex};
auto &global_arena = Metric::get_global_arena(); auto &global_arena = Metric::get_global_arena();
@@ -709,8 +701,7 @@ Family<Histogram> create_histogram(std::string name, std::string help,
} else { } else {
validate_or_abort( validate_or_abort(
family_ptr->help == help, family_ptr->help == help,
"metric family already registered with different help text", "metric family already registered with different help text", name);
name.c_str());
std::vector<double> new_buckets_vec(buckets.begin(), buckets.end()); std::vector<double> new_buckets_vec(buckets.begin(), buckets.end());
std::sort(new_buckets_vec.begin(), new_buckets_vec.end()); std::sort(new_buckets_vec.begin(), new_buckets_vec.end());
new_buckets_vec.erase( new_buckets_vec.erase(
@@ -731,7 +722,7 @@ Family<Histogram> create_histogram(std::string name, std::string help,
} }
validate_or_abort(buckets_match, validate_or_abort(buckets_match,
"metric family already registered with different buckets", "metric family already registered with different buckets",
name.c_str()); name);
} }
Family<Histogram> family; Family<Histogram> family;
family.p = family_ptr; family.p = family_ptr;
@@ -740,9 +731,9 @@ Family<Histogram> create_histogram(std::string name, std::string help,
std::vector<double> linear_buckets(double start, double width, int count) { std::vector<double> linear_buckets(double start, double width, int count) {
validate_or_abort(width > 0, "linear bucket width must be positive", validate_or_abort(width > 0, "linear bucket width must be positive",
std::to_string(width).c_str()); std::to_string(width));
validate_or_abort(count >= 0, "linear bucket count must be non-negative", validate_or_abort(count >= 0, "linear bucket count must be non-negative",
std::to_string(count).c_str()); std::to_string(count));
std::vector<double> buckets; std::vector<double> buckets;
buckets.reserve(count); buckets.reserve(count);
@@ -757,11 +748,11 @@ std::vector<double> linear_buckets(double start, double width, int count) {
std::vector<double> exponential_buckets(double start, double factor, std::vector<double> exponential_buckets(double start, double factor,
int count) { int count) {
validate_or_abort(start > 0, "exponential bucket start must be positive", validate_or_abort(start > 0, "exponential bucket start must be positive",
std::to_string(start).c_str()); std::to_string(start));
validate_or_abort(factor > 1, "exponential bucket factor must be > 1", validate_or_abort(factor > 1, "exponential bucket factor must be > 1",
std::to_string(factor).c_str()); std::to_string(factor));
validate_or_abort(count >= 0, "exponential bucket count must be non-negative", validate_or_abort(count >= 0, "exponential bucket count must be non-negative",
std::to_string(count).c_str()); std::to_string(count));
std::vector<double> buckets; std::vector<double> buckets;
buckets.reserve(count); buckets.reserve(count);
@@ -777,7 +768,7 @@ std::vector<double> exponential_buckets(double start, double factor,
// Prometheus validation functions // Prometheus validation functions
// Metric names must match [a-zA-Z_:][a-zA-Z0-9_:]* // Metric names must match [a-zA-Z_:][a-zA-Z0-9_:]*
bool is_valid_metric_name(const std::string &name) { bool is_valid_metric_name(std::string_view name) {
if (name.empty()) if (name.empty())
return false; return false;
@@ -799,7 +790,7 @@ bool is_valid_metric_name(const std::string &name) {
} }
// Label keys must match [a-zA-Z_][a-zA-Z0-9_]* // Label keys must match [a-zA-Z_][a-zA-Z0-9_]*
bool is_valid_label_key(const std::string &key) { bool is_valid_label_key(std::string_view key) {
if (key.empty()) if (key.empty())
return false; return false;
@@ -826,10 +817,10 @@ bool is_valid_label_key(const std::string &key) {
} }
// Label values can contain any UTF-8 characters (no specific restrictions) // Label values can contain any UTF-8 characters (no specific restrictions)
bool is_valid_label_value(const std::string &value) { bool is_valid_label_value(std::string_view value) {
// Prometheus allows any UTF-8 string as label value // Prometheus allows any UTF-8 string as label value
// Validate UTF-8 encoding for correctness using simdutf // Validate UTF-8 encoding for correctness using simdutf
return simdutf::validate_utf8(value.c_str(), value.size()); return simdutf::validate_utf8(value.data(), value.size());
} }
std::span<std::string_view> render(ArenaAllocator &arena) { std::span<std::string_view> render(ArenaAllocator &arena) {
@@ -1169,33 +1160,30 @@ std::span<std::string_view> render(ArenaAllocator &arena) {
// Template specialization implementations for register_callback // Template specialization implementations for register_callback
template <> template <>
void Family<Counter>::register_callback( void Family<Counter>::register_callback(
std::vector<std::pair<std::string, std::string>> labels, std::span<const std::pair<std::string_view, std::string_view>> labels,
MetricCallback<Counter> callback) { MetricCallback<Counter> callback) {
std::unique_lock<std::mutex> _{Metric::mutex}; std::unique_lock<std::mutex> _{Metric::mutex};
LabelsKey key{labels, Metric::get_global_arena()}; LabelsKey key{labels, Metric::get_global_arena()};
// Validate that labels aren't already in use by create() calls // Validate that labels aren't already in use by create() calls
for (const auto &[thread_id, per_thread] : p->per_thread_state) { for (const auto &[thread_id, per_thread] : p->per_thread_state) {
validate_or_abort( validate_or_abort(per_thread.instances.find(key) ==
per_thread.instances.find(key) == per_thread.instances.end(), per_thread.instances.end(),
"labels already registered as static instance", "labels already registered as static instance",
key.labels.empty() ? "(no labels)" key.labels.empty() ? "(no labels)" : key.labels[0].first);
: std::string(key.labels[0].first).c_str());
} }
// Validate that callback isn't already registered for these labels // Validate that callback isn't already registered for these labels
validate_or_abort(p->callbacks.find(key) == p->callbacks.end(), validate_or_abort(p->callbacks.find(key) == p->callbacks.end(),
"callback already registered for labels", "callback already registered for labels",
key.labels.empty() key.labels.empty() ? "(no labels)" : key.labels[0].first);
? "(no labels)"
: std::string(key.labels[0].first).c_str());
p->callbacks[std::move(key)] = std::move(callback); p->callbacks[std::move(key)] = std::move(callback);
} }
template <> template <>
void Family<Gauge>::register_callback( void Family<Gauge>::register_callback(
std::vector<std::pair<std::string, std::string>> labels, std::span<const std::pair<std::string_view, std::string_view>> labels,
MetricCallback<Gauge> callback) { MetricCallback<Gauge> callback) {
std::unique_lock<std::mutex> _{Metric::mutex}; std::unique_lock<std::mutex> _{Metric::mutex};
LabelsKey key{labels, Metric::get_global_arena()}; LabelsKey key{labels, Metric::get_global_arena()};
@@ -1203,16 +1191,12 @@ void Family<Gauge>::register_callback(
// Validate that labels aren't already in use by create() calls // Validate that labels aren't already in use by create() calls
validate_or_abort(p->instances.find(key) == p->instances.end(), validate_or_abort(p->instances.find(key) == p->instances.end(),
"labels already registered as static instance", "labels already registered as static instance",
key.labels.empty() key.labels.empty() ? "(no labels)" : key.labels[0].first);
? "(no labels)"
: std::string(key.labels[0].first).c_str());
// Validate that callback isn't already registered for these labels // Validate that callback isn't already registered for these labels
validate_or_abort(p->callbacks.find(key) == p->callbacks.end(), validate_or_abort(p->callbacks.find(key) == p->callbacks.end(),
"callback already registered for labels", "callback already registered for labels",
key.labels.empty() key.labels.empty() ? "(no labels)" : key.labels[0].first);
? "(no labels)"
: std::string(key.labels[0].first).c_str());
p->callbacks[std::move(key)] = std::move(callback); p->callbacks[std::move(key)] = std::move(callback);
} }

View File

@@ -36,8 +36,8 @@
// histogram.observe(0.25); // ONLY call from creating thread // histogram.observe(0.25); // ONLY call from creating thread
#include <functional> #include <functional>
#include <initializer_list>
#include <span> #include <span>
#include <string>
#include <type_traits> #include <type_traits>
#include <vector> #include <vector>
@@ -127,20 +127,36 @@ template <class T> struct Family {
// Labels are sorted by key for Prometheus compatibility // Labels are sorted by key for Prometheus compatibility
// ERROR: Will abort if labels already registered via register_callback() // ERROR: Will abort if labels already registered via register_callback()
// OK: Multiple calls with same labels return same instance (idempotent) // OK: Multiple calls with same labels return same instance (idempotent)
T create(std::vector<std::pair<std::string, std::string>> labels); T create(std::initializer_list<std::pair<std::string_view, std::string_view>>
labels) {
return create(
std::span<const std::pair<std::string_view, std::string_view>>(
labels.begin(), labels.end()));
}
T create(
std::span<const std::pair<std::string_view, std::string_view>> labels);
// Register callback-based metric (Counter and Gauge only) // Register callback-based metric (Counter and Gauge only)
// Validates that label set isn't already taken // Validates that label set isn't already taken
void void register_callback(
register_callback(std::vector<std::pair<std::string, std::string>> labels, std::initializer_list<std::pair<std::string_view, std::string_view>>
MetricCallback<T> callback); labels,
MetricCallback<T> callback) {
register_callback(
std::span<const std::pair<std::string_view, std::string_view>>(
labels.begin(), labels.end()),
callback);
}
void register_callback(
std::span<const std::pair<std::string_view, std::string_view>> labels,
MetricCallback<T> callback);
private: private:
Family(); Family();
friend struct Metric; friend struct Metric;
friend Family<Counter> create_counter(std::string, std::string); friend Family<Counter> create_counter(std::string_view, std::string_view);
friend Family<Gauge> create_gauge(std::string, std::string); friend Family<Gauge> create_gauge(std::string_view, std::string_view);
friend Family<Histogram> create_histogram(std::string, std::string, friend Family<Histogram> create_histogram(std::string_view, std::string_view,
std::span<const double>); std::span<const double>);
struct State; struct State;
@@ -153,19 +169,25 @@ private:
// Create counter family (monotonically increasing values) // Create counter family (monotonically increasing values)
// ERROR: Aborts if family with same name is registered with different help // ERROR: Aborts if family with same name is registered with different help
// text. // text.
Family<Counter> create_counter(std::string name, std::string help); Family<Counter> create_counter(std::string_view name, std::string_view help);
// Create gauge family (can increase/decrease) // Create gauge family (can increase/decrease)
// ERROR: Aborts if family with same name is registered with different help // ERROR: Aborts if family with same name is registered with different help
// text. // text.
Family<Gauge> create_gauge(std::string name, std::string help); Family<Gauge> create_gauge(std::string_view name, std::string_view help);
// Create histogram family with custom buckets // Create histogram family with custom buckets
// Buckets will be sorted, deduplicated, and +Inf will be added automatically // Buckets will be sorted, deduplicated, and +Inf will be added automatically
// ERROR: Aborts if family with same name is registered with different help text // ERROR: Aborts if family with same name is registered with different help text
// or buckets. // or buckets.
Family<Histogram> create_histogram(std::string name, std::string help, Family<Histogram> create_histogram(std::string_view name, std::string_view help,
std::span<const double> buckets); std::span<const double> buckets);
inline Family<Histogram>
create_histogram(std::string_view name, std::string_view help,
std::initializer_list<double> buckets) {
return create_histogram(
name, help, std::span<const double>(buckets.begin(), buckets.end()));
}
// Helper functions for generating standard histogram buckets // Helper functions for generating standard histogram buckets
// Following Prometheus client library conventions // Following Prometheus client library conventions
@@ -189,9 +211,9 @@ std::vector<double> exponential_buckets(double start, double factor, int count);
std::span<std::string_view> render(ArenaAllocator &arena); std::span<std::string_view> render(ArenaAllocator &arena);
// Validation functions for Prometheus compatibility // Validation functions for Prometheus compatibility
bool is_valid_metric_name(const std::string &name); bool is_valid_metric_name(std::string_view name);
bool is_valid_label_key(const std::string &key); bool is_valid_label_key(std::string_view key);
bool is_valid_label_value(const std::string &value); bool is_valid_label_value(std::string_view value);
// Note: Histograms do not support callbacks due to their multi-value nature // Note: Histograms do not support callbacks due to their multi-value nature
// (buckets + sum + count). Use static histogram metrics only. // (buckets + sum + count). Use static histogram metrics only.