#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 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 #include #include #include #include #include "arena.hpp" #include "reference.hpp" namespace metric { // Forward declarations template 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 using MetricCallback = std::function; // 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 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 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 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 struct Family { static_assert(std::is_same_v || std::is_same_v || std::is_same_v); // 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> labels) { return create( std::span>( labels.begin(), labels.end())); } T create( std::span> labels); // Register callback-based metric (Counter and Gauge only) // Validates that label set isn't already taken void register_callback( std::initializer_list> labels, MetricCallback callback) { register_callback( std::span>( labels.begin(), labels.end()), callback); } void register_callback( std::span> labels, MetricCallback callback); private: Family(); friend struct Metric; friend Family create_counter(std::string_view, std::string_view); friend Family create_gauge(std::string_view, std::string_view); friend Family create_histogram(std::string_view, std::string_view, std::span); 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 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 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 create_histogram(std::string_view name, std::string_view help, std::span buckets); inline Family create_histogram(std::string_view name, std::string_view help, std::initializer_list buckets) { return create_histogram( name, help, std::span(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 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 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 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); // Note: Histograms do not support callbacks due to their multi-value nature // (buckets + sum + count). Use static histogram metrics only. } // namespace metric