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
static void validate_or_abort(bool condition, const char *message,
const char *value) {
std::string_view value) {
if (!condition) {
std::fprintf(stderr, "WeaselDB metric validation failed: %s: '%s'\n",
message, value);
std::fprintf(stderr, "WeaselDB metric validation failed: %s: '%.*s'\n",
message, static_cast<int>(value.size()), value.data());
std::abort();
}
}
// 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) {
if (str.empty()) {
return std::string_view{};
}
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';
return std::string_view(copied, str.size());
}
@@ -81,15 +81,14 @@ static std::string_view arena_copy_string(const std::string &str,
struct LabelsKey {
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)
: labels(&arena) {
// Copy and validate all label keys and values into arena
for (const auto &[key, value] : l) {
validate_or_abort(is_valid_label_key(key), "invalid label key",
key.c_str());
validate_or_abort(is_valid_label_key(key), "invalid label key", key);
validate_or_abort(is_valid_label_value(value), "invalid label value",
value.c_str());
value);
auto key_view = arena_copy_string(key, arena);
auto value_view = arena_copy_string(value, arena);
@@ -382,19 +381,18 @@ struct Metric {
static Counter create_counter_instance(
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
(void)thread_init;
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_or_abort(
family->p->callbacks.find(key) == family->p->callbacks.end(),
validate_or_abort(family->p->callbacks.find(key) ==
family->p->callbacks.end(),
"labels already registered as callback",
key.labels.empty() ? "(no labels)"
: std::string(key.labels[0].first).c_str());
key.labels.empty() ? "(no labels)" : key.labels[0].first);
// Ensure thread state exists
auto thread_id = std::this_thread::get_id();
@@ -419,16 +417,15 @@ struct Metric {
static Gauge create_gauge_instance(
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};
LabelsKey key{labels, get_global_arena()};
// Validate that labels aren't already registered as callback
validate_or_abort(
family->p->callbacks.find(key) == family->p->callbacks.end(),
validate_or_abort(family->p->callbacks.find(key) ==
family->p->callbacks.end(),
"labels already registered as callback",
key.labels.empty() ? "(no labels)"
: std::string(key.labels[0].first).c_str());
key.labels.empty() ? "(no labels)" : key.labels[0].first);
auto &ptr = family->p->instances[key];
if (!ptr) {
@@ -442,12 +439,12 @@ struct Metric {
static Histogram create_histogram_instance(
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
(void)thread_init;
std::unique_lock<std::mutex> _{mutex};
LabelsKey key{labels, get_thread_local_arena()};
LabelsKey key{labels, get_global_arena()};
// Ensure thread state exists
auto thread_id = std::this_thread::get_id();
@@ -512,7 +509,7 @@ void Counter::inc(double x) {
// Validate monotonic property (counter never decreases)
if (new_value < p->value) [[unlikely]] {
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);
@@ -605,25 +602,24 @@ template <> Family<Histogram>::Family() = default;
template <>
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);
}
template <>
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);
}
template <>
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);
}
Family<Counter> create_counter(std::string name, std::string help) {
validate_or_abort(is_valid_metric_name(name), "invalid counter name",
name.c_str());
Family<Counter> create_counter(std::string_view name, std::string_view help) {
validate_or_abort(is_valid_metric_name(name), "invalid counter name", name);
std::unique_lock<std::mutex> _{Metric::mutex};
auto &global_arena = Metric::get_global_arena();
@@ -640,17 +636,15 @@ Family<Counter> create_counter(std::string name, std::string help) {
} else {
validate_or_abort(
familyPtr->help == help,
"metric family already registered with different help text",
name.c_str());
"metric family already registered with different help text", name);
}
Family<Counter> family;
family.p = familyPtr;
return family;
}
Family<Gauge> create_gauge(std::string name, std::string help) {
validate_or_abort(is_valid_metric_name(name), "invalid gauge name",
name.c_str());
Family<Gauge> create_gauge(std::string_view name, std::string_view help) {
validate_or_abort(is_valid_metric_name(name), "invalid gauge name", name);
std::unique_lock<std::mutex> _{Metric::mutex};
auto &global_arena = Metric::get_global_arena();
@@ -667,18 +661,16 @@ Family<Gauge> create_gauge(std::string name, std::string help) {
} else {
validate_or_abort(
familyPtr->help == help,
"metric family already registered with different help text",
name.c_str());
"metric family already registered with different help text", name);
}
Family<Gauge> family;
family.p = familyPtr;
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) {
validate_or_abort(is_valid_metric_name(name), "invalid histogram name",
name.c_str());
validate_or_abort(is_valid_metric_name(name), "invalid histogram name", name);
std::unique_lock<std::mutex> _{Metric::mutex};
auto &global_arena = Metric::get_global_arena();
@@ -709,8 +701,7 @@ Family<Histogram> create_histogram(std::string name, std::string help,
} else {
validate_or_abort(
family_ptr->help == help,
"metric family already registered with different help text",
name.c_str());
"metric family already registered with different help text", name);
std::vector<double> new_buckets_vec(buckets.begin(), buckets.end());
std::sort(new_buckets_vec.begin(), new_buckets_vec.end());
new_buckets_vec.erase(
@@ -731,7 +722,7 @@ Family<Histogram> create_histogram(std::string name, std::string help,
}
validate_or_abort(buckets_match,
"metric family already registered with different buckets",
name.c_str());
name);
}
Family<Histogram> family;
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) {
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",
std::to_string(count).c_str());
std::to_string(count));
std::vector<double> buckets;
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,
int count) {
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",
std::to_string(factor).c_str());
std::to_string(factor));
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;
buckets.reserve(count);
@@ -777,7 +768,7 @@ std::vector<double> exponential_buckets(double start, double factor,
// Prometheus validation functions
// 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())
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_]*
bool is_valid_label_key(const std::string &key) {
bool is_valid_label_key(std::string_view key) {
if (key.empty())
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)
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
// 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) {
@@ -1169,33 +1160,30 @@ std::span<std::string_view> render(ArenaAllocator &arena) {
// Template specialization implementations for register_callback
template <>
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) {
std::unique_lock<std::mutex> _{Metric::mutex};
LabelsKey key{labels, Metric::get_global_arena()};
// Validate that labels aren't already in use by create() calls
for (const auto &[thread_id, per_thread] : p->per_thread_state) {
validate_or_abort(
per_thread.instances.find(key) == per_thread.instances.end(),
validate_or_abort(per_thread.instances.find(key) ==
per_thread.instances.end(),
"labels already registered as static instance",
key.labels.empty() ? "(no labels)"
: std::string(key.labels[0].first).c_str());
key.labels.empty() ? "(no labels)" : key.labels[0].first);
}
// Validate that callback isn't already registered for these labels
validate_or_abort(p->callbacks.find(key) == p->callbacks.end(),
"callback already registered for labels",
key.labels.empty()
? "(no labels)"
: std::string(key.labels[0].first).c_str());
key.labels.empty() ? "(no labels)" : key.labels[0].first);
p->callbacks[std::move(key)] = std::move(callback);
}
template <>
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) {
std::unique_lock<std::mutex> _{Metric::mutex};
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_or_abort(p->instances.find(key) == p->instances.end(),
"labels already registered as static instance",
key.labels.empty()
? "(no labels)"
: std::string(key.labels[0].first).c_str());
key.labels.empty() ? "(no labels)" : key.labels[0].first);
// Validate that callback isn't already registered for these labels
validate_or_abort(p->callbacks.find(key) == p->callbacks.end(),
"callback already registered for labels",
key.labels.empty()
? "(no labels)"
: std::string(key.labels[0].first).c_str());
key.labels.empty() ? "(no labels)" : key.labels[0].first);
p->callbacks[std::move(key)] = std::move(callback);
}

View File

@@ -36,8 +36,8 @@
// histogram.observe(0.25); // ONLY call from creating thread
#include <functional>
#include <initializer_list>
#include <span>
#include <string>
#include <type_traits>
#include <vector>
@@ -127,20 +127,36 @@ template <class T> struct Family {
// Labels are sorted by key for Prometheus compatibility
// ERROR: Will abort if labels already registered via register_callback()
// 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)
// Validates that label set isn't already taken
void
register_callback(std::vector<std::pair<std::string, std::string>> labels,
void register_callback(
std::initializer_list<std::pair<std::string_view, std::string_view>>
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:
Family();
friend struct Metric;
friend Family<Counter> create_counter(std::string, std::string);
friend Family<Gauge> create_gauge(std::string, std::string);
friend Family<Histogram> create_histogram(std::string, std::string,
friend Family<Counter> create_counter(std::string_view, std::string_view);
friend Family<Gauge> create_gauge(std::string_view, std::string_view);
friend Family<Histogram> create_histogram(std::string_view, std::string_view,
std::span<const double>);
struct State;
@@ -153,19 +169,25 @@ private:
// Create counter family (monotonically increasing values)
// ERROR: Aborts if family with same name is registered with different help
// 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)
// ERROR: Aborts if family with same name is registered with different help
// 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
// Buckets will be sorted, deduplicated, and +Inf will be added automatically
// ERROR: Aborts if family with same name is registered with different help text
// 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);
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
// 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);
// Validation functions for Prometheus compatibility
bool is_valid_metric_name(const std::string &name);
bool is_valid_label_key(const std::string &key);
bool is_valid_label_value(const std::string &value);
bool is_valid_metric_name(std::string_view name);
bool is_valid_label_key(std::string_view key);
bool is_valid_label_value(std::string_view value);
// Note: Histograms do not support callbacks due to their multi-value nature
// (buckets + sum + count). Use static histogram metrics only.