Validation + callback api

This commit is contained in:
2025-08-29 11:31:06 -04:00
parent b6d4ae2862
commit e3a2ddbbfb
2 changed files with 198 additions and 19 deletions

View File

@@ -1,4 +1,5 @@
#include "metric.hpp"
#include <simdutf.h>
// WeaselDB Metrics System Design:
//
@@ -21,7 +22,10 @@
#include <atomic>
#include <bit>
#include <cassert>
#include <cctype>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <limits>
#include <memory>
#include <mutex>
@@ -33,12 +37,30 @@
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) {
if (!condition) {
std::fprintf(stderr, "WeaselDB metric validation failed: %s: '%s'\n",
message, value);
std::abort();
}
}
// Labels key for second level of map
struct LabelsKey {
std::vector<std::pair<std::string, std::string>> labels;
LabelsKey(std::vector<std::pair<std::string, std::string>> l)
: labels(std::move(l)) {
// Validate all label keys and values
for (const auto &[key, value] : labels) {
validate_or_abort(is_valid_label_key(key), "invalid label key",
key.c_str());
validate_or_abort(is_valid_label_value(value), "invalid label value",
value.c_str());
}
// Sort labels by key for Prometheus compatibility
std::sort(labels.begin(), labels.end(),
[](const auto &a, const auto &b) { return a.first < b.first; });
@@ -85,12 +107,18 @@ template <> struct Family<Counter>::State {
std::unordered_map<LabelsKey, std::unique_ptr<Counter::State>> instances;
};
std::unordered_map<std::thread::id, PerThreadState> perThreadState;
// Callback-based metrics (global, not per-thread)
std::unordered_map<LabelsKey, MetricCallback<Counter>> callbacks;
};
template <> struct Family<Gauge>::State {
std::string name;
std::string help;
std::unordered_map<LabelsKey, std::unique_ptr<Gauge::State>> instances;
// Callback-based metrics
std::unordered_map<LabelsKey, MetricCallback<Gauge>> callbacks;
};
template <> struct Family<Histogram>::State {
@@ -102,6 +130,8 @@ template <> struct Family<Histogram>::State {
std::unordered_map<LabelsKey, std::unique_ptr<Histogram::State>> instances;
};
std::unordered_map<std::thread::id, PerThreadState> perThreadState;
// Note: No callbacks map - histograms don't support callback-based metrics
};
// Counter: Thread-local, monotonically increasing, single writer per thread
@@ -110,10 +140,9 @@ struct Counter::State {
friend struct Metric;
};
// Gauge: Global, can increase/decrease, multiple writers (requires mutex)
// Gauge: Global, can increase/decrease, multiple writers (uses atomic CAS)
struct Gauge::State {
std::mutex mutex;
double value; // Plain double, protected by mutex
AtomicWord value; // Stores double as uint64_t bits, lock-free
friend struct Metric;
};
@@ -172,6 +201,13 @@ struct Metric {
const std::vector<std::pair<std::string, std::string>> &labels) {
std::unique_lock<std::mutex> _{mutex};
LabelsKey key{labels};
// Validate that labels aren't already registered as callback
validate_or_abort(
family->p->callbacks.find(key) == family->p->callbacks.end(),
"labels already registered as callback",
key.labels.empty() ? "(no labels)" : key.labels[0].first.c_str());
auto &ptr =
family->p->perThreadState[std::this_thread::get_id()].instances[key];
if (!ptr) {
@@ -188,10 +224,17 @@ struct Metric {
const std::vector<std::pair<std::string, std::string>> &labels) {
std::unique_lock<std::mutex> _{mutex};
LabelsKey key{labels};
// Validate that labels aren't already registered as callback
validate_or_abort(
family->p->callbacks.find(key) == family->p->callbacks.end(),
"labels already registered as callback",
key.labels.empty() ? "(no labels)" : key.labels[0].first.c_str());
auto &ptr = family->p->instances[key];
if (!ptr) {
ptr = std::make_unique<Gauge::State>();
ptr->value = 0.0;
ptr->value.store(0, std::memory_order_relaxed);
}
Gauge result;
result.p = ptr.get();
@@ -228,28 +271,47 @@ struct Metric {
};
void Counter::inc(double x) {
assert(x >= 0);
validate_or_abort(x >= 0, "counter increment must be >= 0",
std::to_string(x).c_str());
// DESIGN: Single writer per thread allows simple load-modify-store
// No CAS loop needed since only one thread writes to this counter
auto current_value =
std::bit_cast<double>(p->value.load(std::memory_order_relaxed));
p->value.store(std::bit_cast<uint64_t>(current_value + x),
std::memory_order_relaxed);
auto new_value = current_value + x;
// Validate monotonic property (counter never decreases)
validate_or_abort(new_value >= current_value,
"counter value overflow/wraparound detected",
std::to_string(new_value).c_str());
p->value.store(std::bit_cast<uint64_t>(new_value), std::memory_order_relaxed);
}
void Gauge::inc(double x) {
// IMPLEMENTATION DETAIL: Mutex protection used internally for thread safety,
// but API contract remains single-writer per instance
std::unique_lock<std::mutex> _{p->mutex};
p->value += x;
// Lock-free increment using CAS loop
uint64_t expected = p->value.load(std::memory_order_relaxed);
uint64_t desired;
do {
double current_value = std::bit_cast<double>(expected);
double new_value = current_value + x;
desired = std::bit_cast<uint64_t>(new_value);
} while (!p->value.compare_exchange_weak(expected, desired,
std::memory_order_relaxed));
}
void Gauge::dec(double x) {
std::unique_lock<std::mutex> _{p->mutex};
p->value -= x;
// Lock-free decrement using CAS loop
uint64_t expected = p->value.load(std::memory_order_relaxed);
uint64_t desired;
do {
double current_value = std::bit_cast<double>(expected);
double new_value = current_value - x;
desired = std::bit_cast<uint64_t>(new_value);
} while (!p->value.compare_exchange_weak(expected, desired,
std::memory_order_relaxed));
}
void Gauge::set(double x) {
std::unique_lock<std::mutex> _{p->mutex};
p->value = x;
// Simple atomic store for set operation
p->value.store(std::bit_cast<uint64_t>(x), std::memory_order_relaxed);
}
void Histogram::observe(double x) {
assert(p->thresholds.size() == p->counts.size());
@@ -289,6 +351,9 @@ Histogram Family<Histogram>::create(
}
Family<Counter> create_counter(std::string name, std::string help) {
validate_or_abort(is_valid_metric_name(name), "invalid counter name",
name.c_str());
std::unique_lock<std::mutex> _{Metric::mutex};
auto &familyPtr = Metric::counterFamilies[name];
if (!familyPtr) {
@@ -302,6 +367,9 @@ Family<Counter> create_counter(std::string name, std::string help) {
}
Family<Gauge> create_gauge(std::string name, std::string help) {
validate_or_abort(is_valid_metric_name(name), "invalid gauge name",
name.c_str());
std::unique_lock<std::mutex> _{Metric::mutex};
auto &familyPtr = Metric::gaugeFamilies[name];
if (!familyPtr) {
@@ -316,6 +384,9 @@ Family<Gauge> create_gauge(std::string name, std::string help) {
Family<Histogram> create_histogram(std::string name, std::string help,
std::initializer_list<double> buckets) {
validate_or_abort(is_valid_metric_name(name), "invalid histogram name",
name.c_str());
std::unique_lock<std::mutex> _{Metric::mutex};
auto &familyPtr = Metric::histogramFamilies[name];
if (!familyPtr) {
@@ -340,10 +411,69 @@ Family<Histogram> create_histogram(std::string name, std::string help,
return family;
}
std::span<std::string> render(ArenaAllocator &arena) {
// Prometheus validation functions
// Metric names must match [a-zA-Z_:][a-zA-Z0-9_:]*
bool is_valid_metric_name(const std::string &name) {
if (name.empty())
return false;
// First character must be letter, underscore, or colon
char first = name[0];
if (!std::isalpha(first) && first != '_' && first != ':') {
return false;
}
// Remaining characters must be alphanumeric, underscore, or colon
for (size_t i = 1; i < name.size(); ++i) {
char c = name[i];
if (!std::isalnum(c) && c != '_' && c != ':') {
return false;
}
}
return true;
}
// Label keys must match [a-zA-Z_][a-zA-Z0-9_]*
bool is_valid_label_key(const std::string &key) {
if (key.empty())
return false;
// First character must be letter or underscore
char first = key[0];
if (!std::isalpha(first) && first != '_') {
return false;
}
// Remaining characters must be alphanumeric or underscore
for (size_t i = 1; i < key.size(); ++i) {
char c = key[i];
if (!std::isalnum(c) && c != '_') {
return false;
}
}
// Label keys starting with __ are reserved for internal use
if (key.size() >= 2 && key[0] == '_' && key[1] == '_') {
return false;
}
return true;
}
// Label values can contain any UTF-8 characters (no specific restrictions)
bool is_valid_label_value(const std::string &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());
}
std::span<std::string_view> render(ArenaAllocator &arena) {
// TODO: Implement Prometheus text format rendering
static std::string empty_result = "";
return std::span<std::string>(&empty_result, 0);
// All string data should be allocated in the arena and returned as
// string_views
static std::string_view empty_result = "";
return std::span<std::string_view>(&empty_result, 0);
}
// Static member definitions