270 lines
10 KiB
C++
270 lines
10 KiB
C++
#pragma once
|
|
|
|
// WeaselDB Metrics System
|
|
//
|
|
// High-performance metrics collection with Prometheus-compatible output.
|
|
//
|
|
// DESIGN PRINCIPLES:
|
|
// - Single-writer semantics: Each metric instance bound to creating thread
|
|
// - Lock-free operations using atomic<uint64_t> storage for doubles
|
|
// - Full IEEE 754 double precision preservation via bit reinterpretation
|
|
// - Single global registry: All metrics registered in one global namespace
|
|
//
|
|
// CRITICAL THREAD SAFETY CONSTRAINT:
|
|
// Each metric instance has exactly ONE writer thread (the creating thread).
|
|
// It is undefined behavior to call inc()/dec()/set()/observe() from a different
|
|
// thread.
|
|
//
|
|
// REGISTRY MODEL:
|
|
// This implementation uses a single global registry for all metrics, unlike
|
|
// typical Prometheus client libraries that support multiple registries.
|
|
// This design choice prioritizes simplicity and performance over flexibility.
|
|
//
|
|
// PERFORMANCE NOTE:
|
|
// Family registration operations (create_counter/gauge/histogram), metric
|
|
// instance creation (.create()), and render() use a global mutex for thread
|
|
// safety. Registration operations should be performed during application
|
|
// initialization, not in performance-critical paths. Metric update operations
|
|
// (inc/dec/set/observe) are designed for high-frequency use and do not contend
|
|
// on the global mutex.
|
|
//
|
|
// METRIC LIFECYCLE:
|
|
// Metrics are created once and persist for the application lifetime. There is
|
|
// no unregistration mechanism - this prevents accidental metric loss and
|
|
// simplifies the implementation.
|
|
//
|
|
// USAGE:
|
|
// auto counter_family = metric::create_counter("requests_total", "Total
|
|
// requests"); auto counter = counter_family.create({{"method", "GET"}}); //
|
|
// Bound to this thread counter.inc(1.0); // ONLY call from creating thread
|
|
//
|
|
// auto histogram_family = metric::create_histogram("latency", "Request
|
|
// latency", {0.1, 0.5, 1.0}); auto histogram =
|
|
// histogram_family.create({{"endpoint", "/api"}}); // Bound to this thread
|
|
// histogram.observe(0.25); // ONLY call from creating thread
|
|
|
|
#include <functional>
|
|
#include <initializer_list>
|
|
#include <span>
|
|
#include <type_traits>
|
|
#include <vector>
|
|
|
|
#include "arena.hpp"
|
|
#include "reference.hpp"
|
|
|
|
namespace metric {
|
|
|
|
// Forward declarations
|
|
template <typename T> struct Family;
|
|
|
|
// Callback function type for dynamic metric values
|
|
// Called during render() to get current metric value
|
|
// THREAD SAFETY: May be called from arbitrary thread, but serialized by
|
|
// render() mutex - no need to be thread-safe internally
|
|
template <typename T> using MetricCallback = std::function<double()>;
|
|
|
|
// Counter: A metric value that only increases.
|
|
//
|
|
// THREAD SAFETY RULES:
|
|
// 1. Do not call inc() on the same Counter object from multiple threads.
|
|
// Each object must have only one writer thread.
|
|
// 2. To use Counters concurrently, each thread must create its own Counter
|
|
// object.
|
|
// 3. When rendered, the values of all Counter objects with the same labels
|
|
// are summed together into a single total.
|
|
struct Counter {
|
|
void inc(double = 1.0); // Increment counter (must be >= 0, never blocks)
|
|
|
|
private:
|
|
Counter();
|
|
friend struct Metric;
|
|
template <class> friend struct Family;
|
|
struct State;
|
|
State *p;
|
|
};
|
|
|
|
// Gauge: A metric value that can be set, increased, or decreased.
|
|
//
|
|
// THREAD SAFETY RULES:
|
|
// 1. Do not call inc(), dec(), or set() on the same Gauge object from
|
|
// multiple threads. Each object must have only one writer thread.
|
|
// 2. To use Gauges concurrently, each thread must create its own Gauge object.
|
|
// 3. If multiple Gauge objects are created with the same labels, their
|
|
// operations are combined. For example, increments from different objects
|
|
// are cumulative.
|
|
// 4. For independent gauges, create them with unique labels.
|
|
struct Gauge {
|
|
void inc(double = 1.0); // (never blocks)
|
|
void dec(double = 1.0); // (never blocks)
|
|
void set(double); // (never blocks)
|
|
|
|
private:
|
|
Gauge();
|
|
friend struct Metric;
|
|
template <class> friend struct Family;
|
|
struct State;
|
|
State *p;
|
|
};
|
|
|
|
// Histogram: A metric that samples observations into buckets.
|
|
//
|
|
// THREAD SAFETY RULES:
|
|
// 1. Do not call observe() on the same Histogram object from multiple
|
|
// threads. Each object must have only one writer thread.
|
|
// 2. To use Histograms concurrently, each thread must create its own
|
|
// Histogram object.
|
|
// 3. When rendered, the observations from all Histogram objects with the
|
|
// same labels are combined into a single histogram.
|
|
struct Histogram {
|
|
void
|
|
observe(double); // Record observation in appropriate bucket (never blocks)
|
|
|
|
private:
|
|
Histogram();
|
|
friend struct Metric;
|
|
template <class> friend struct Family;
|
|
struct State;
|
|
State *p;
|
|
};
|
|
|
|
// Family: Factory for creating metric instances with different label
|
|
// combinations Each family represents one metric name with varying labels
|
|
template <class T> struct Family {
|
|
static_assert(std::is_same_v<T, Counter> || std::is_same_v<T, Gauge> ||
|
|
std::is_same_v<T, Histogram>);
|
|
|
|
// Create metric instance with specific labels.
|
|
// For performance, it is recommended to create instances once and cache them
|
|
// for reuse, rather than calling .create() repeatedly in
|
|
// performance-critical paths.
|
|
//
|
|
// 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::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::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_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;
|
|
State *p;
|
|
};
|
|
|
|
// Factory functions for creating metric families
|
|
|
|
// 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_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_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_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
|
|
|
|
// Generate linear buckets: start, start+width, start+2*width, ...,
|
|
// start+(count-1)*width Example: linear_buckets(0, 10, 5) = {0, 10, 20, 30, 40}
|
|
std::vector<double> linear_buckets(double start, double width, int count);
|
|
|
|
// Generate exponential buckets: start, start*factor, start*factor^2, ...,
|
|
// start*factor^(count-1) Example: exponential_buckets(1, 2, 5) = {1, 2, 4, 8,
|
|
// 16}
|
|
std::vector<double> exponential_buckets(double start, double factor, int count);
|
|
|
|
// Render all metrics in Prometheus text format
|
|
// Returns chunks of Prometheus exposition format (includes # HELP and # TYPE
|
|
// lines) Each string_view may contain multiple lines separated by '\n' String
|
|
// views are NOT null-terminated - use .size() for length All string data
|
|
// allocated in provided arena for zero-copy efficiency. The caller is
|
|
// responsible for the arena's lifecycle. THREAD SAFETY: Serialized by global
|
|
// mutex - callbacks need not be thread-safe
|
|
std::span<std::string_view> render(Arena &arena);
|
|
|
|
// Validation functions for Prometheus compatibility
|
|
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);
|
|
|
|
// Reset all metrics state - WARNING: Only safe for testing!
|
|
// This clears all registered families and metrics. Should only be called
|
|
// when no metric objects are in use and no concurrent render() calls.
|
|
void reset_metrics_for_testing();
|
|
|
|
/**
|
|
* @brief Interface for a custom collector that can be registered with the
|
|
* metrics system.
|
|
*
|
|
* This is used for complex metric gathering, such as reading from /proc, where
|
|
* multiple metrics need to be updated from a single data source.
|
|
*/
|
|
struct Collector {
|
|
/**
|
|
* @brief Virtual destructor.
|
|
*/
|
|
virtual ~Collector() = default;
|
|
|
|
/**
|
|
* @brief Called by the metrics system to update the metrics this collector is
|
|
* responsible for.
|
|
*/
|
|
virtual void collect() = 0;
|
|
};
|
|
|
|
/**
|
|
* @brief Register a collector with the metrics system.
|
|
*
|
|
* The system will hold a Ref to the collector and call its collect()
|
|
* method during each metric rendering.
|
|
*
|
|
* @param collector A Ref to the collector to be registered.
|
|
*/
|
|
void register_collector(Ref<Collector> collector);
|
|
|
|
// Note: Histograms do not support callbacks due to their multi-value nature
|
|
// (buckets + sum + count). Use static histogram metrics only.
|
|
|
|
} // namespace metric
|