diff --git a/.github/workflows/linux_llvm_cov.yml b/.github/workflows/linux_llvm_cov.yml index b712449c..54135d57 100644 --- a/.github/workflows/linux_llvm_cov.yml +++ b/.github/workflows/linux_llvm_cov.yml @@ -40,9 +40,10 @@ jobs: ./test_corofile ./test_http_parse ./test_time_util + ./test_metric llvm-profdata merge -sparse test_cinatra-*.profraw -o test_cinatra.profdata - llvm-cov show test_cinatra -object test_corofile -object test_time_util -object test_http_parse -instr-profile=test_cinatra.profdata -format=html -output-dir=../.coverage_llvm_cov -ignore-filename-regex="example|asio|cmdline|async_simple|tests" -show-instantiations=false + llvm-cov show test_cinatra -object test_corofile -object test_time_util -object test_http_parse -object test_metric -instr-profile=test_cinatra.profdata -format=html -output-dir=../.coverage_llvm_cov -ignore-filename-regex="example|asio|cmdline|async_simple|tests" -show-instantiations=false echo "Done!" - name: Upload Coverage Results diff --git a/cmake/develop.cmake b/cmake/develop.cmake index bf186e25..51d3174b 100644 --- a/cmake/develop.cmake +++ b/cmake/develop.cmake @@ -53,6 +53,12 @@ if(ENABLE_SANITIZER AND NOT MSVC) endif() endif() +option(ENABLE_METRIC_JSON "Enable serialize metric to json" OFF) +if(ENABLE_METRIC_JSON) + add_definitions(-DCINATRA_ENABLE_METRIC_JSON) + message(STATUS "Enable serialize metric to json") +endif() + set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_MODULE_PATH}") SET(ENABLE_GZIP OFF) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index e1c2b029..3be33fd6 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -6,7 +6,9 @@ set(CINATRA_EXAMPLE ) include_directories(../include) -include_directories(../include/cinatra) +if(ENABLE_METRIC_JSON) + include_directories(../iguana) +endif() add_executable(${project_name} ${CINATRA_EXAMPLE}) target_compile_definitions(${project_name} PRIVATE ASYNC_SIMPLE_HAS_NOT_AIO) diff --git a/example/main.cpp b/example/main.cpp index 8af75539..3a1ed04d 100644 --- a/example/main.cpp +++ b/example/main.cpp @@ -7,10 +7,10 @@ #include #include "../include/cinatra.hpp" -#include "metric_conf.hpp" +#include "cinatra/metric_conf.hpp" using namespace cinatra; -using namespace ylt; +using namespace ylt::metric; using namespace std::chrono_literals; void create_file(std::string filename, size_t file_size = 64) { @@ -429,7 +429,7 @@ async_simple::coro::Lazy basic_usage() { } void use_metric() { - using namespace ylt; + using namespace ylt::metric; auto c = std::make_shared("request_count", "request count", std::vector{"method", "url"}); @@ -537,7 +537,7 @@ void metrics_example() { "/", [&](coro_http_request &req, coro_http_response &resp) { resp.set_status_and_content(status_type::ok, "hello world"); }); - server.use_metrics(); + server.use_metrics(true, "/metrics"); server.sync_start(); } diff --git a/iguana b/iguana index 043512e3..90e9f4a8 160000 --- a/iguana +++ b/iguana @@ -1 +1 @@ -Subproject commit 043512e380fa794f09f1d38bf36bfb533c0d47dd +Subproject commit 90e9f4a824aba660bbcaf47a23b7955d4468e3bb diff --git a/include/cinatra/coro_http_server.hpp b/include/cinatra/coro_http_server.hpp index 29031eae..17a057fd 100644 --- a/include/cinatra/coro_http_server.hpp +++ b/include/cinatra/coro_http_server.hpp @@ -182,12 +182,23 @@ class coro_http_server { } } - void use_metrics(std::string url_path = "/metrics") { + void use_metrics(bool enable_json = false, + std::string url_path = "/metrics") { init_metrics(); set_http_handler( - url_path, [](coro_http_request &req, coro_http_response &res) { - std::string str = async_simple::coro::syncAwait( - ylt::default_metric_manager::serialize_static()); + url_path, + [enable_json](coro_http_request &req, coro_http_response &res) { + std::string str; +#ifdef CINATRA_ENABLE_METRIC_JSON + if (enable_json) { + str = + ylt::metric::default_metric_manager::serialize_to_json_static(); + res.set_content_type(); + } + else +#endif + str = ylt::metric::default_metric_manager::serialize_static(); + res.set_status_and_content(status_type::ok, std::move(str)); }); } @@ -900,7 +911,7 @@ class coro_http_server { private: void init_metrics() { - using namespace ylt; + using namespace ylt::metric; cinatra_metric_conf::enable_metric = true; default_metric_manager::create_metric_static( diff --git a/include/cinatra/metric_conf.hpp b/include/cinatra/metric_conf.hpp index 549c93cd..4800481a 100644 --- a/include/cinatra/metric_conf.hpp +++ b/include/cinatra/metric_conf.hpp @@ -25,9 +25,8 @@ struct cinatra_metric_conf { return; } - static auto m = - ylt::default_metric_manager::get_metric_static( - server_total_req); + static auto m = ylt::metric::default_metric_manager::get_metric_static< + ylt::metric::counter_t>(server_total_req); if (m == nullptr) { return; } @@ -38,9 +37,8 @@ struct cinatra_metric_conf { if (!enable_metric) { return; } - static auto m = - ylt::default_metric_manager::get_metric_static( - server_failed_req); + static auto m = ylt::metric::default_metric_manager::get_metric_static< + ylt::metric::counter_t>(server_failed_req); if (m == nullptr) { return; } @@ -51,9 +49,8 @@ struct cinatra_metric_conf { if (!enable_metric) { return; } - static auto m = - ylt::default_metric_manager::get_metric_static( - server_total_fd); + static auto m = ylt::metric::default_metric_manager::get_metric_static< + ylt::metric::gauge_t>(server_total_fd); if (m == nullptr) { return; } @@ -64,9 +61,8 @@ struct cinatra_metric_conf { if (!enable_metric) { return; } - static auto m = - ylt::default_metric_manager::get_metric_static( - server_total_fd); + static auto m = ylt::metric::default_metric_manager::get_metric_static< + ylt::metric::gauge_t>(server_total_fd); if (m == nullptr) { return; } @@ -77,9 +73,8 @@ struct cinatra_metric_conf { if (!enable_metric) { return; } - static auto m = - ylt::default_metric_manager::get_metric_static( - server_total_recv_bytes); + static auto m = ylt::metric::default_metric_manager::get_metric_static< + ylt::metric::counter_t>(server_total_recv_bytes); if (m == nullptr) { return; } @@ -90,9 +85,8 @@ struct cinatra_metric_conf { if (!enable_metric) { return; } - static auto m = - ylt::default_metric_manager::get_metric_static( - server_total_send_bytes); + static auto m = ylt::metric::default_metric_manager::get_metric_static< + ylt::metric::counter_t>(server_total_send_bytes); if (m == nullptr) { return; } @@ -103,9 +97,8 @@ struct cinatra_metric_conf { if (!enable_metric) { return; } - static auto m = - ylt::default_metric_manager::get_metric_static( - server_req_latency); + static auto m = ylt::metric::default_metric_manager::get_metric_static< + ylt::metric::histogram_t>(server_req_latency); if (m == nullptr) { return; } @@ -116,9 +109,8 @@ struct cinatra_metric_conf { if (!enable_metric) { return; } - static auto m = - ylt::default_metric_manager::get_metric_static( - server_read_latency); + static auto m = ylt::metric::default_metric_manager::get_metric_static< + ylt::metric::histogram_t>(server_read_latency); if (m == nullptr) { return; } diff --git a/include/cinatra/ylt/coro_io/io_context_pool.hpp b/include/cinatra/ylt/coro_io/io_context_pool.hpp index 08018980..ab1e6bef 100644 --- a/include/cinatra/ylt/coro_io/io_context_pool.hpp +++ b/include/cinatra/ylt/coro_io/io_context_pool.hpp @@ -285,6 +285,18 @@ inline T &g_io_context_pool( return *_g_io_context_pool; } +template +inline std::shared_ptr create_io_context_pool( + unsigned pool_size = std::thread::hardware_concurrency()) { + auto pool = std::make_shared(pool_size); + std::thread thrd{[pool] { + pool->run(); + }}; + thrd.detach(); + + return pool; +} + template inline T &g_block_io_context_pool( unsigned pool_size = std::thread::hardware_concurrency()) { diff --git a/include/cinatra/ylt/metric/counter.hpp b/include/cinatra/ylt/metric/counter.hpp index a252106d..d801722a 100644 --- a/include/cinatra/ylt/metric/counter.hpp +++ b/include/cinatra/ylt/metric/counter.hpp @@ -4,13 +4,23 @@ #include "metric.hpp" -namespace ylt { +namespace ylt::metric { enum class op_type_t { INC, DEC, SET }; -struct counter_sample { - op_type_t op_type; - std::vector labels_value; - double value; + +#ifdef CINATRA_ENABLE_METRIC_JSON +struct json_counter_metric_t { + std::unordered_multimap labels; + int64_t value; +}; +REFLECTION(json_counter_metric_t, labels, value); +struct json_counter_t { + std::string name; + std::string help; + std::string type; + std::vector metrics; }; +REFLECTION(json_counter_t, name, help, type, metrics); +#endif class counter_t : public metric_t { public: @@ -23,12 +33,8 @@ class counter_t : public metric_t { // static labels value, contains a map with atomic value. counter_t(std::string name, std::string help, std::map labels) - : metric_t(MetricType::Counter, std::move(name), std::move(help)) { - for (auto &[k, v] : labels) { - labels_name_.push_back(k); - labels_value_.push_back(v); - } - + : metric_t(MetricType::Counter, std::move(name), std::move(help), + std::move(labels)) { atomic_value_map_.emplace(labels_value_, 0); use_atomic_ = true; } @@ -54,12 +60,8 @@ class counter_t : public metric_t { } } - std::map, double, - std::less>> - value_map() { - std::map, double, - std::less>> - map; + metric_hash_map value_map() override { + metric_hash_map map; if (use_atomic_) { map = {atomic_value_map_.begin(), atomic_value_map_.end()}; } @@ -97,6 +99,44 @@ class counter_t : public metric_t { } } +#ifdef CINATRA_ENABLE_METRIC_JSON + void serialize_to_json(std::string &str) override { + std::string s; + if (labels_name_.empty()) { + if (default_lable_value_ == 0) { + return; + } + json_counter_t counter{name_, help_, std::string(metric_name())}; + int64_t value = default_lable_value_; + counter.metrics.push_back({{}, value}); + iguana::to_json(counter, str); + return; + } + + json_counter_t counter{name_, help_, std::string(metric_name())}; + if (use_atomic_) { + to_json(counter, atomic_value_map_, str); + } + else { + to_json(counter, value_map_, str); + } + } + + template + void to_json(json_counter_t &counter, T &map, std::string &str) { + for (auto &[k, v] : map) { + json_counter_metric_t metric; + size_t index = 0; + for (auto &label_value : k) { + metric.labels.emplace(labels_name_[index++], label_value); + } + metric.value = (int64_t)v; + counter.metrics.push_back(std::move(metric)); + } + iguana::to_json(counter, str); + } +#endif + void inc(double val = 1) { if (val < 0) { throw std::invalid_argument("the value is less than zero"); @@ -148,9 +188,7 @@ class counter_t : public metric_t { } } - std::map, std::atomic, - std::less>> - &atomic_value_map() { + metric_hash_map> &atomic_value_map() { return atomic_value_map_; } @@ -223,7 +261,12 @@ class counter_t : public metric_t { label_val += value; } #else - label_val += value; + if constexpr (is_atomic) { + label_val.fetch_add(value, std::memory_order_relaxed); + } + else { + label_val += value; + } #endif } break; case op_type_t::DEC: @@ -234,9 +277,13 @@ class counter_t : public metric_t { else { label_val -= value; } - #else - label_val -= value; + if constexpr (is_atomic) { + label_val.fetch_sub(value, std::memory_order_relaxed); + } + else { + label_val -= value; + } #endif break; case op_type_t::SET: @@ -245,14 +292,10 @@ class counter_t : public metric_t { } } - std::map, std::atomic, - std::less>> - atomic_value_map_; + metric_hash_map> atomic_value_map_; std::atomic default_lable_value_ = 0; std::mutex mtx_; - std::map, double, - std::less>> - value_map_; + metric_hash_map value_map_; }; -} // namespace ylt \ No newline at end of file +} // namespace ylt::metric \ No newline at end of file diff --git a/include/cinatra/ylt/metric/detail/ckms_quantiles.hpp b/include/cinatra/ylt/metric/detail/ckms_quantiles.hpp index cdaf1db9..fd42a8c2 100644 --- a/include/cinatra/ylt/metric/detail/ckms_quantiles.hpp +++ b/include/cinatra/ylt/metric/detail/ckms_quantiles.hpp @@ -5,7 +5,7 @@ // https://github.com/jupp0r/prometheus-cpp/blob/master/core/include/prometheus/detail/ckms_quantiles.h -namespace ylt { +namespace ylt::metric { class CKMSQuantiles { public: struct Quantile { @@ -172,4 +172,4 @@ class CKMSQuantiles { std::array buffer_; std::size_t buffer_count_; }; -} // namespace ylt \ No newline at end of file +} // namespace ylt::metric \ No newline at end of file diff --git a/include/cinatra/ylt/metric/detail/time_window_quantiles.hpp b/include/cinatra/ylt/metric/detail/time_window_quantiles.hpp index fd7df105..6b81179f 100644 --- a/include/cinatra/ylt/metric/detail/time_window_quantiles.hpp +++ b/include/cinatra/ylt/metric/detail/time_window_quantiles.hpp @@ -2,7 +2,7 @@ #include "ckms_quantiles.hpp" // https://github.com/jupp0r/prometheus-cpp/blob/master/core/include/prometheus/detail/time_window_quantiles.h -namespace ylt { +namespace ylt::metric { class TimeWindowQuantiles { using Clock = std::chrono::steady_clock; @@ -49,4 +49,4 @@ class TimeWindowQuantiles { mutable Clock::time_point last_rotation_; const Clock::duration rotation_interval_; }; -} // namespace ylt \ No newline at end of file +} // namespace ylt::metric \ No newline at end of file diff --git a/include/cinatra/ylt/metric/gauge.hpp b/include/cinatra/ylt/metric/gauge.hpp index 19c4b65c..18693f1a 100644 --- a/include/cinatra/ylt/metric/gauge.hpp +++ b/include/cinatra/ylt/metric/gauge.hpp @@ -3,7 +3,7 @@ #include "counter.hpp" -namespace ylt { +namespace ylt::metric { class gauge_t : public counter_t { public: gauge_t(std::string name, std::string help) @@ -49,4 +49,4 @@ class gauge_t : public counter_t { } } }; -} // namespace ylt \ No newline at end of file +} // namespace ylt::metric \ No newline at end of file diff --git a/include/cinatra/ylt/metric/histogram.hpp b/include/cinatra/ylt/metric/histogram.hpp index dfd2ca13..1f371b85 100644 --- a/include/cinatra/ylt/metric/histogram.hpp +++ b/include/cinatra/ylt/metric/histogram.hpp @@ -8,7 +8,24 @@ #include "counter.hpp" #include "metric.hpp" -namespace ylt { +namespace ylt::metric { +#ifdef CINATRA_ENABLE_METRIC_JSON +struct json_histogram_metric_t { + std::map labels; + std::map quantiles; + int64_t count; + double sum; +}; +REFLECTION(json_histogram_metric_t, labels, quantiles, count, sum); +struct json_histogram_t { + std::string name; + std::string help; + std::string type; + std::vector metrics; +}; +REFLECTION(json_histogram_t, name, help, type, metrics); +#endif + class histogram_t : public metric_t { public: histogram_t(std::string name, std::string help, std::vector buckets) @@ -25,7 +42,41 @@ class histogram_t : public metric_t { use_atomic_ = true; } + histogram_t(std::string name, std::string help, std::vector buckets, + std::vector labels_name) + : bucket_boundaries_(buckets), + metric_t(MetricType::Histogram, name, help, labels_name), + sum_(std::make_shared(name, help, labels_name)) { + if (!is_strict_sorted(begin(bucket_boundaries_), end(bucket_boundaries_))) { + throw std::invalid_argument("Bucket Boundaries must be strictly sorted"); + } + + for (size_t i = 0; i < buckets.size() + 1; i++) { + bucket_counts_.push_back( + std::make_shared(name, help, labels_name)); + } + } + + histogram_t(std::string name, std::string help, std::vector buckets, + std::map labels) + : bucket_boundaries_(buckets), + metric_t(MetricType::Histogram, name, help, labels), + sum_(std::make_shared(name, help, labels)) { + if (!is_strict_sorted(begin(bucket_boundaries_), end(bucket_boundaries_))) { + throw std::invalid_argument("Bucket Boundaries must be strictly sorted"); + } + + for (size_t i = 0; i < buckets.size() + 1; i++) { + bucket_counts_.push_back(std::make_shared(name, help, labels)); + } + use_atomic_ = true; + } + void observe(double value) { + if (!use_atomic_ || !labels_name_.empty()) { + throw std::invalid_argument("not a default label metric"); + } + const auto bucket_index = static_cast( std::distance(bucket_boundaries_.begin(), std::lower_bound(bucket_boundaries_.begin(), @@ -34,9 +85,29 @@ class histogram_t : public metric_t { bucket_counts_[bucket_index]->inc(); } + void observe(const std::vector &labels_value, double value) { + if (sum_->labels_name().empty()) { + throw std::invalid_argument("not a label metric"); + } + + const auto bucket_index = static_cast( + std::distance(bucket_boundaries_.begin(), + std::lower_bound(bucket_boundaries_.begin(), + bucket_boundaries_.end(), value))); + sum_->inc(labels_value, value); + bucket_counts_[bucket_index]->inc(labels_value); + } + auto get_bucket_counts() { return bucket_counts_; } - void serialize(std::string& str) override { + metric_hash_map value_map() override { return sum_->value_map(); } + + void serialize(std::string &str) override { + if (!sum_->labels_name().empty()) { + serialize_with_labels(str); + return; + } + serialize_head(str); double count = 0; auto bucket_counts = get_bucket_counts(); @@ -68,6 +139,41 @@ class histogram_t : public metric_t { .append("\n"); } +#ifdef CINATRA_ENABLE_METRIC_JSON + void serialize_to_json(std::string &str) override { + if (!sum_->labels_name().empty()) { + serialize_to_json_with_labels(str); + return; + } + + json_histogram_t hist{name_, help_, std::string(metric_name())}; + + double count = 0; + auto bucket_counts = get_bucket_counts(); + json_histogram_metric_t metric{}; + for (size_t i = 0; i < bucket_counts.size(); i++) { + auto counter = bucket_counts[i]; + + count += counter->value(); + + if (i == bucket_boundaries_.size()) { + metric.quantiles.emplace(std::numeric_limits::max(), + (int64_t)count); + } + else { + metric.quantiles.emplace(bucket_boundaries_[i], + (int64_t)counter->value()); + } + } + metric.count = (int64_t)count; + metric.sum = sum_->value(); + + hist.metrics.push_back(std::move(metric)); + + iguana::to_json(hist, str); + } +#endif + private: template bool is_strict_sorted(ForwardIterator first, ForwardIterator last) { @@ -76,8 +182,102 @@ class histogram_t : public metric_t { ForwardIterator>::value_type>()) == last; } + void serialize_with_labels(std::string &str) { + serialize_head(str); + + auto bucket_counts = get_bucket_counts(); + + auto value_map = sum_->value_map(); + for (auto &[labels_value, value] : value_map) { + if (value == 0) { + continue; + } + + double count = 0; + for (size_t i = 0; i < bucket_counts.size(); i++) { + auto counter = bucket_counts[i]; + str.append(name_).append("_bucket{"); + build_label_string(str, sum_->labels_name(), labels_value); + str.append(","); + + if (i == bucket_boundaries_.size()) { + str.append("le=\"").append("+Inf").append("\"} "); + } + else { + str.append("le=\"") + .append(std::to_string(bucket_boundaries_[i])) + .append("\"} "); + } + + count += counter->value(labels_value); + str.append(std::to_string(count)); + str.append("\n"); + } + + str.append(name_); + str.append("_sum{"); + build_label_string(str, sum_->labels_name(), labels_value); + str.append("} "); + + if (type_ == MetricType::Counter) { + str.append(std::to_string((int64_t)value)); + } + else { + str.append(std::to_string(value)); + } + str.append("\n"); + + str.append(name_).append("_count{"); + build_label_string(str, sum_->labels_name(), labels_value); + str.append("} "); + str.append(std::to_string(count)); + str.append("\n"); + } + } + +#ifdef CINATRA_ENABLE_METRIC_JSON + void serialize_to_json_with_labels(std::string &str) { + json_histogram_t hist{name_, help_, std::string(metric_name())}; + auto bucket_counts = get_bucket_counts(); + + auto value_map = sum_->value_map(); + for (auto &[labels_value, value] : value_map) { + if (value == 0) { + continue; + } + + size_t count = 0; + json_histogram_metric_t metric{}; + for (size_t i = 0; i < bucket_counts.size(); i++) { + auto counter = bucket_counts[i]; + + count += counter->value(labels_value); + + if (i == bucket_boundaries_.size()) { + metric.quantiles.emplace(std::numeric_limits::max(), + (int64_t)count); + } + else { + metric.quantiles.emplace(bucket_boundaries_[i], + (int64_t)counter->value(labels_value)); + } + } + metric.count = (int64_t)count; + metric.sum = sum_->value(labels_value); + + for (size_t i = 0; i < labels_value.size(); i++) { + metric.labels[sum_->labels_name()[i]] = labels_value[i]; + } + + hist.metrics.push_back(std::move(metric)); + } + + iguana::to_json(hist, str); + } +#endif + std::vector bucket_boundaries_; std::vector> bucket_counts_; // readonly std::shared_ptr sum_; }; -} // namespace ylt \ No newline at end of file +} // namespace ylt::metric \ No newline at end of file diff --git a/include/cinatra/ylt/metric/metric.hpp b/include/cinatra/ylt/metric/metric.hpp index 58d0afc5..12c25c6b 100644 --- a/include/cinatra/ylt/metric/metric.hpp +++ b/include/cinatra/ylt/metric/metric.hpp @@ -1,17 +1,38 @@ #pragma once +#include #include #include #include #include #include +#include +#include #include #include #include #include "async_simple/coro/Lazy.h" +#include "async_simple/coro/SyncAwait.h" #include "cinatra/cinatra_log_wrapper.hpp" +#if __has_include("ylt/coro_io/coro_io.hpp") +#include "ylt/coro_io/coro_io.hpp" +#else +#include "cinatra/ylt/coro_io/coro_io.hpp" +#endif + +#ifdef CINATRA_ENABLE_METRIC_JSON +namespace iguana { + +template +inline char* to_chars_float(T value, char* buffer) { + return buffer + snprintf(buffer, 65, "%g", value); +} + +} // namespace iguana -namespace ylt { +#include +#endif +namespace ylt::metric { enum class MetricType { Counter, Gauge, @@ -20,15 +41,54 @@ enum class MetricType { Nil, }; +struct metric_filter_options { + std::optional name_regex{}; + std::optional label_regex{}; + bool is_white = true; +}; + +struct vector_hash { + size_t operator()(const std::vector& vec) const { + unsigned int seed = 131; + unsigned int hash = 0; + + for (const auto& str : vec) { + for (auto ch : str) { + hash = hash * seed + ch; + } + } + + return (hash & 0x7FFFFFFF); + } +}; + +template +using metric_hash_map = + std::unordered_map, T, vector_hash>; + class metric_t { public: metric_t() = default; - metric_t(MetricType type, std::string name, std::string help, - std::vector labels_name = {}) + metric_t(MetricType type, std::string name, std::string help) : type_(type), name_(std::move(name)), help_(std::move(help)), - labels_name_(std::move(labels_name)) {} + metric_created_time_(std::chrono::system_clock::now()) {} + metric_t(MetricType type, std::string name, std::string help, + std::vector labels_name) + : metric_t(type, std::move(name), std::move(help)) { + labels_name_ = std::move(labels_name); + } + + metric_t(MetricType type, std::string name, std::string help, + std::map static_labels) + : metric_t(type, std::move(name), std::move(help)) { + static_labels_ = std::move(static_labels); + for (auto& [k, v] : static_labels_) { + labels_name_.push_back(k); + labels_value_.push_back(v); + } + } virtual ~metric_t() {} std::string_view name() { return name_; } @@ -37,6 +97,8 @@ class metric_t { MetricType metric_type() { return type_; } + auto get_created_time() { return metric_created_time_; } + std::string_view metric_name() { switch (type_) { case MetricType::Counter: @@ -55,13 +117,31 @@ class metric_t { const std::vector& labels_name() { return labels_name_; } + const std::map& get_static_labels() { + return static_labels_; + } + + virtual metric_hash_map value_map() { return {}; } + virtual void serialize(std::string& str) {} +#ifdef CINATRA_ENABLE_METRIC_JSON + virtual void serialize_to_json(std::string& str) {} +#endif + // only for summary virtual async_simple::coro::Lazy serialize_async(std::string& out) { co_return; } +#ifdef CINATRA_ENABLE_METRIC_JSON + // only for summary + virtual async_simple::coro::Lazy serialize_to_json_async( + std::string& out) { + co_return; + } +#endif + bool is_atomic() const { return use_atomic_; } template @@ -80,6 +160,19 @@ class metric_t { .append("\n"); } + void build_label_string(std::string& str, + const std::vector& label_name, + const std::vector& label_value) { + for (size_t i = 0; i < label_name.size(); i++) { + str.append(label_name[i]) + .append("=\"") + .append(label_value[i]) + .append("\"") + .append(","); + } + str.pop_back(); + } + #ifdef __APPLE__ double mac_os_atomic_fetch_add(std::atomic* obj, double arg) { double v; @@ -101,9 +194,11 @@ class metric_t { MetricType type_ = MetricType::Nil; std::string name_; std::string help_; + std::map static_labels_; std::vector labels_name_; // read only std::vector labels_value_; // read only bool use_atomic_ = false; + std::chrono::system_clock::time_point metric_created_time_{}; }; template @@ -131,19 +226,27 @@ struct metric_manager_t { const std::string& help, Args&&... args) { auto m = std::make_shared(name, help, std::forward(args)...); - bool r = register_metric_static(m); + bool r = register_metric_dynamic(m); if (!r) { return nullptr; } return m; } + static bool register_metric_static(std::shared_ptr metric) { + return register_metric_impl(metric); + } + static bool register_metric_dynamic(std::shared_ptr metric) { return register_metric_impl(metric); } - static bool register_metric_static(std::shared_ptr metric) { - return register_metric_impl(metric); + static bool remove_metric_static(const std::string& name) { + return remove_metric_impl(name); + } + + static bool remove_metric_dynamic(const std::string& name) { + return remove_metric_impl(name); } template @@ -175,30 +278,144 @@ struct metric_manager_t { return metric_keys_impl(); } + // static labels: {{"method", "GET"}, {"url", "/"}} + static std::vector> get_metric_by_labels_static( + const std::map& labels) { + std::vector> vec; + auto map = metric_map_static(); + for (auto& [name, m] : map) { + const auto& static_labels = m->get_static_labels(); + if (static_labels == labels) { + vec.push_back(m); + } + } + return vec; + } + + // static label: {"method", "GET"} + static std::vector> get_metric_by_label_static( + const std::pair& label) { + std::vector> vec; + auto map = metric_map_static(); + for (auto& [name, t] : map) { + const auto& static_labels = t->get_static_labels(); + for (const auto& pair : static_labels) { + if (pair.first == label.first && pair.second == label.second) { + vec.push_back(t); + } + } + } + return vec; + } + + // labels: {{"method", "POST"}, {"code", "200"}} + static std::vector> get_metric_by_labels_dynamic( + const std::map& labels) { + std::vector> vec; + auto map = metric_map_dynamic(); + for (auto& [name, t] : map) { + auto val_map = t->value_map(); + auto labels_name = t->labels_name(); + + for (auto& [k, v] : labels) { + if (auto it = std::find(labels_name.begin(), labels_name.end(), k); + it != labels_name.end()) { + if (auto it = std::find_if(val_map.begin(), val_map.end(), + [label_val = v](auto& pair) { + auto& key = pair.first; + return std::find(key.begin(), key.end(), + label_val) != key.end(); + }); + it != val_map.end()) { + vec.push_back(t); + } + } + } + } + + return vec; + } + template - static T* get_metric_static(const std::string& name) { + static std::shared_ptr get_metric_static(const std::string& name) { auto m = get_metric_impl(name); if (m == nullptr) { return nullptr; } - return m->template as(); + return std::dynamic_pointer_cast(m); } template - static T* get_metric_dynamic(const std::string& name) { + static std::shared_ptr get_metric_dynamic(const std::string& name) { auto m = get_metric_impl(name); if (m == nullptr) { return nullptr; } - return m->template as(); + return std::dynamic_pointer_cast(m); } - static async_simple::coro::Lazy serialize_static() { - return serialize_impl(); + static std::string serialize( + const std::vector>& metrics) { + std::string str; + for (auto& m : metrics) { + if (m->metric_type() == MetricType::Summary) { + async_simple::coro::syncAwait(m->serialize_async(str)); + } + else { + m->serialize(str); + } + } + + return str; } - static async_simple::coro::Lazy serialize_dynamic() { - return serialize_impl(); + static std::string serialize_static() { return serialize(collect()); } + + static std::string serialize_dynamic() { return serialize(collect()); } + +#ifdef CINATRA_ENABLE_METRIC_JSON + static std::string serialize_to_json_static() { + auto metrics = collect(); + return serialize_to_json(metrics); + } + + static std::string serialize_to_json_dynamic() { + auto metrics = collect(); + return serialize_to_json(metrics); + } + + static std::string serialize_to_json( + const std::vector>& metrics) { + if (metrics.empty()) { + return ""; + } + std::string str; + str.append("["); + for (auto& m : metrics) { + size_t start = str.size(); + if (m->metric_type() == MetricType::Summary) { + async_simple::coro::syncAwait(m->serialize_to_json_async(str)); + } + else { + m->serialize_to_json(str); + } + + if (str.size() > start) + str.append(","); + } + str.back() = ']'; + return str; + } +#endif + + static std::vector> filter_metrics_static( + const metric_filter_options& options) { + return filter_metrics(options); + } + + static std::vector> filter_metrics_dynamic( + const metric_filter_options& options) { + return filter_metrics(options); } private: @@ -242,6 +459,12 @@ struct metric_manager_t { return r; } + template + static size_t remove_metric_impl(const std::string& name) { + auto lock = get_lock(); + return metric_map_.erase(name); + } + template static auto metric_map_impl() { auto lock = get_lock(); @@ -289,19 +512,64 @@ struct metric_manager_t { return metrics; } - template - static async_simple::coro::Lazy serialize_impl() { - std::string str; + static void filter_by_label_name( + std::vector>& filtered_metrics, + std::shared_ptr m, const metric_filter_options& options, + std::vector& indexs, size_t index) { + const auto& labels_name = m->labels_name(); + for (auto& label_name : labels_name) { + if (std::regex_match(label_name, *options.label_regex)) { + if (options.is_white) { + filtered_metrics.push_back(m); + } + else { + indexs.push_back(index); + } + } + } + } + + template + static std::vector> filter_metrics( + const metric_filter_options& options) { auto metrics = collect(); + if (!options.name_regex && !options.label_regex) { + return metrics; + } + + std::vector> filtered_metrics; + std::vector indexs; + size_t index = 0; for (auto& m : metrics) { - if (m->metric_type() == MetricType::Summary) { - co_await m->serialize_async(str); + if (options.name_regex && !options.label_regex) { + if (std::regex_match(std::string(m->name()), *options.name_regex)) { + if (options.is_white) { + filtered_metrics.push_back(m); + } + else { + indexs.push_back(index); + } + } + } + else if (options.label_regex && !options.name_regex) { + filter_by_label_name(filtered_metrics, m, options, indexs, index); } else { - m->serialize(str); + if (std::regex_match(std::string(m->name()), *options.name_regex)) { + filter_by_label_name(filtered_metrics, m, options, indexs, index); + } + } + index++; + } + + if (!options.is_white) { + for (size_t i : indexs) { + metrics.erase(std::next(metrics.begin(), i)); } + return metrics; } - co_return str; + + return filtered_metrics; } static inline std::mutex mtx_; @@ -313,4 +581,4 @@ struct metric_manager_t { }; using default_metric_manager = metric_manager_t<0>; -} // namespace ylt \ No newline at end of file +} // namespace ylt::metric \ No newline at end of file diff --git a/include/cinatra/ylt/metric/summary.hpp b/include/cinatra/ylt/metric/summary.hpp index 415d6b32..58957bc7 100644 --- a/include/cinatra/ylt/metric/summary.hpp +++ b/include/cinatra/ylt/metric/summary.hpp @@ -3,10 +3,35 @@ #include "detail/time_window_quantiles.hpp" #include "metric.hpp" -#include "ylt/coro_io/coro_io.hpp" +#if __has_include("ylt/util/concurrentqueue.h") #include "ylt/util/concurrentqueue.h" +#else +#include "cinatra/ylt/util/concurrentqueue.h" +#endif + +namespace ylt::metric { +#ifdef CINATRA_ENABLE_METRIC_JSON +struct json_summary_metric_t { + std::map labels; + std::map quantiles; + int64_t count; + double sum; +}; +REFLECTION(json_summary_metric_t, labels, quantiles, count, sum); +struct json_summary_t { + std::string name; + std::string help; + std::string type; + std::vector metrics; +}; +REFLECTION(json_summary_t, name, help, type, metrics); +#endif + +struct summary_label_sample { + std::vector labels_value; + double value; +}; -namespace ylt { class summary_t : public metric_t { public: using Quantiles = std::vector; @@ -14,24 +39,52 @@ class summary_t : public metric_t { std::chrono::milliseconds max_age = std::chrono::seconds{60}, int age_buckets = 5) : quantiles_{std::move(quantiles)}, - metric_t(MetricType::Summary, std::move(name), std::move(help)) { - work_ = std::make_shared(ctx_); - thd_ = std::thread([this] { - ctx_.run(); - }); - excutor_ = - std::make_unique>(ctx_.get_executor()); - block_ = std::make_shared(); + metric_t(MetricType::Summary, std::move(name), std::move(help)), + max_age_(max_age), + age_buckets_(age_buckets) { + init_block(block_); block_->quantile_values_ = std::make_shared(quantiles_, max_age, age_buckets); - start_timer(block_).via(excutor_.get()).start([](auto &&) { - }); + use_atomic_ = true; + } + + summary_t(std::string name, std::string help, Quantiles quantiles, + std::vector labels_name, + std::chrono::milliseconds max_age = std::chrono::seconds{60}, + int age_buckets = 5) + : quantiles_{std::move(quantiles)}, + metric_t(MetricType::Summary, std::move(name), std::move(help), + std::move(labels_name)), + max_age_(max_age), + age_buckets_(age_buckets) { + init_block(labels_block_); + } + + summary_t(std::string name, std::string help, Quantiles quantiles, + std::map static_labels, + std::chrono::milliseconds max_age = std::chrono::seconds{60}, + int age_buckets = 5) + : quantiles_{std::move(quantiles)}, + metric_t(MetricType::Summary, std::move(name), std::move(help), + std::move(static_labels)), + max_age_(max_age), + age_buckets_(age_buckets) { + init_block(labels_block_); + labels_block_->label_quantile_values_[labels_value_] = + std::make_shared(quantiles_, max_age, age_buckets); + labels_block_->label_count_.emplace(labels_value_, 0); + labels_block_->label_sum_.emplace(labels_value_, 0); + use_atomic_ = true; } ~summary_t() { - block_->stop_ = true; - work_ = nullptr; - thd_.join(); + if (block_) { + block_->stop_ = true; + } + + if (labels_block_) { + labels_block_->stop_ = true; + } } struct block_t { @@ -42,7 +95,53 @@ class summary_t : public metric_t { double sum_; }; - void observe(double value) { block_->sample_queue_.enqueue(value); } + struct labels_block_t { + std::atomic stop_ = false; + moodycamel::ConcurrentQueue sample_queue_; + metric_hash_map> + label_quantile_values_; + metric_hash_map label_count_; + metric_hash_map label_sum_; + }; + + void observe(double value) { + if (!labels_name_.empty()) { + throw std::invalid_argument("not a default label metric"); + } + if (block_->sample_queue_.size_approx() >= 20000000) { + // TODO: record failed count. + return; + } + block_->sample_queue_.enqueue(value); + + bool expected = false; + if (is_coro_started_.compare_exchange_strong(expected, true)) { + start(block_).via(excutor_->get_executor()).start([](auto &&) { + }); + } + } + + void observe(std::vector labels_value, double value) { + if (labels_value.empty()) { + throw std::invalid_argument("not a label metric"); + } + if (use_atomic_) { + if (labels_value != labels_value_) { + throw std::invalid_argument("not equal with static label"); + } + } + if (labels_block_->sample_queue_.size_approx() >= 20000000) { + // TODO: record failed count. + return; + } + labels_block_->sample_queue_.enqueue({std::move(labels_value), value}); + + bool expected = false; + if (is_coro_started_.compare_exchange_strong(expected, true)) { + start(labels_block_).via(excutor_->get_executor()).start([](auto &&) { + }); + } + } async_simple::coro::Lazy> get_rates(double &sum, uint64_t &count) { @@ -51,34 +150,84 @@ class summary_t : public metric_t { co_return std::vector{}; } - co_await coro_io::post([this, &vec, &sum, &count] { - sum = block_->sum_; - count = block_->count_; - for (const auto &quantile : quantiles_) { - vec.push_back(block_->quantile_values_->get(quantile.quantile)); + co_await coro_io::post( + [this, &vec, &sum, &count] { + sum = block_->sum_; + count = block_->count_; + for (const auto &quantile : quantiles_) { + vec.push_back(block_->quantile_values_->get(quantile.quantile)); + } + }, + excutor_->get_executor()); + + co_return vec; + } + + async_simple::coro::Lazy> get_rates( + const std::vector &labels_value, double &sum, + uint64_t &count) { + std::vector vec; + if (quantiles_.empty()) { + co_return std::vector{}; + } + + if (use_atomic_) { + if (labels_value != labels_value_) { + throw std::invalid_argument("not equal with static label"); } - }); + } + + co_await coro_io::post( + [this, &vec, &sum, &count, &labels_value] { + auto it = labels_block_->label_quantile_values_.find(labels_value); + if (it == labels_block_->label_quantile_values_.end()) { + return; + } + sum = labels_block_->label_sum_[labels_value]; + count = labels_block_->label_count_[labels_value]; + for (const auto &quantile : quantiles_) { + vec.push_back(it->second->get(quantile.quantile)); + } + }, + excutor_->get_executor()); co_return vec; } + metric_hash_map value_map() override { + auto ret = async_simple::coro::syncAwait(coro_io::post( + [this] { + return labels_block_->label_sum_; + }, + excutor_->get_executor())); + return ret.value(); + } + async_simple::coro::Lazy get_sum() { - auto ret = co_await coro_io::post([this] { - return block_->sum_; - }); + auto ret = co_await coro_io::post( + [this] { + return block_->sum_; + }, + excutor_->get_executor()); co_return ret.value(); } async_simple::coro::Lazy get_count() { - auto ret = co_await coro_io::post([this] { - return block_->count_; - }); + auto ret = co_await coro_io::post( + [this] { + return block_->count_; + }, + excutor_->get_executor()); co_return ret.value(); } size_t size_approx() { return block_->sample_queue_.size_approx(); } async_simple::coro::Lazy serialize_async(std::string &str) override { + if (block_ == nullptr) { + co_await serialize_async_with_label(str); + co_return; + } if (quantiles_.empty()) { co_return; } @@ -103,8 +252,46 @@ class summary_t : public metric_t { .append("\n"); } +#ifdef CINATRA_ENABLE_METRIC_JSON + async_simple::coro::Lazy serialize_to_json_async( + std::string &str) override { + if (block_ == nullptr) { + co_await serialize_to_json_with_label_async(str); + co_return; + } + + if (quantiles_.empty()) { + co_return; + } + + json_summary_t summary{name_, help_, std::string(metric_name())}; + double sum = 0; + uint64_t count = 0; + auto rates = co_await get_rates(sum, count); + + json_summary_metric_t metric; + + for (size_t i = 0; i < quantiles_.size(); i++) { + metric.quantiles.emplace(quantiles_[i].quantile, rates[i]); + } + + metric.sum = sum; + metric.count = count; + + summary.metrics.push_back(std::move(metric)); + + iguana::to_json(summary, str); + } +#endif private: - async_simple::coro::Lazy start_timer(std::shared_ptr block) { + template + void init_block(std::shared_ptr &block) { + block = std::make_shared(); + start(block).via(excutor_->get_executor()).start([](auto &&) { + }); + } + + async_simple::coro::Lazy start(std::shared_ptr block) { double sample; size_t count = 1000000; while (!block->stop_) { @@ -119,21 +306,154 @@ class summary_t : public metric_t { } } + if (block->sample_queue_.size_approx() == 0) { + is_coro_started_ = false; + if (block->sample_queue_.size_approx() == 0) { + break; + } + + bool expected = false; + if (!is_coro_started_.compare_exchange_strong(expected, true)) { + break; + } + + continue; + } + co_await async_simple::coro::Yield{}; + } - if (block->sample_queue_.size_approx() == 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(5)); + co_return; + } + + async_simple::coro::Lazy start( + std::shared_ptr label_block) { + summary_label_sample sample; + size_t count = 1000000; + while (!label_block->stop_) { + size_t index = 0; + while (label_block->sample_queue_.try_dequeue(sample)) { + auto &ptr = label_block->label_quantile_values_[sample.labels_value]; + + if (ptr == nullptr) { + ptr = std::make_shared(quantiles_, max_age_, + age_buckets_); + } + + ptr->insert(sample.value); + + label_block->label_count_[sample.labels_value] += 1; + label_block->label_sum_[sample.labels_value] += sample.value; + index++; + if (index == count) { + break; + } } + + co_await async_simple::coro::Yield{}; + + if (label_block->sample_queue_.size_approx() == 0) { + is_coro_started_ = false; + if (label_block->sample_queue_.size_approx() == 0) { + break; + } + + bool expected = false; + if (!is_coro_started_.compare_exchange_strong(expected, true)) { + break; + } + + continue; + } + co_await async_simple::coro::Yield{}; } co_return; } + async_simple::coro::Lazy serialize_async_with_label(std::string &str) { + if (quantiles_.empty()) { + co_return; + } + + serialize_head(str); + + auto sum_map = co_await coro_io::post( + [this] { + return labels_block_->label_sum_; + }, + excutor_->get_executor()); + + for (auto &[labels_value, sum_val] : sum_map.value()) { + double sum = 0; + uint64_t count = 0; + auto rates = co_await get_rates(labels_value, sum, count); + for (size_t i = 0; i < quantiles_.size(); i++) { + str.append(name_); + str.append("{"); + build_label_string(str, labels_name_, labels_value); + str.append(","); + str.append("quantile=\""); + str.append(std::to_string(quantiles_[i].quantile)).append("\"} "); + str.append(std::to_string(rates[i])).append("\n"); + } + + str.append(name_).append("_sum "); + str.append("{"); + build_label_string(str, labels_name_, labels_value); + str.append("} "); + str.append(std::to_string(sum)).append("\n"); + + str.append(name_).append("_count "); + str.append("{"); + build_label_string(str, labels_name_, labels_value); + str.append("} "); + str.append(std::to_string((uint64_t)count)).append("\n"); + } + } + +#ifdef CINATRA_ENABLE_METRIC_JSON + async_simple::coro::Lazy serialize_to_json_with_label_async( + std::string &str) { + if (quantiles_.empty()) { + co_return; + } + + auto sum_map = co_await coro_io::post( + [this] { + return labels_block_->label_sum_; + }, + excutor_->get_executor()); + + json_summary_t summary{name_, help_, std::string(metric_name())}; + + for (auto &[labels_value, sum_val] : sum_map.value()) { + json_summary_metric_t metric; + double sum = 0; + uint64_t count = 0; + auto rates = co_await get_rates(labels_value, sum, count); + metric.count = count; + metric.sum = sum; + for (size_t i = 0; i < quantiles_.size(); i++) { + for (size_t i = 0; i < labels_value.size(); i++) { + metric.labels[labels_name_[i]] = labels_value[i]; + } + metric.quantiles.emplace(quantiles_[i].quantile, rates[i]); + } + + summary.metrics.push_back(std::move(metric)); + } + iguana::to_json(summary, str); + } +#endif + Quantiles quantiles_; // readonly std::shared_ptr block_; - std::unique_ptr> excutor_ = nullptr; - std::shared_ptr work_; - asio::io_context ctx_; - std::thread thd_; + std::shared_ptr labels_block_; + static inline std::shared_ptr excutor_ = + coro_io::create_io_context_pool(1); + std::chrono::milliseconds max_age_; + int age_buckets_; + std::atomic is_coro_started_ = false; }; -} // namespace ylt \ No newline at end of file +} // namespace ylt::metric \ No newline at end of file diff --git a/lang/how_to_use_metrics.md b/lang/how_to_use_metrics.md index ac880bf3..471a6b5a 100644 --- a/lang/how_to_use_metrics.md +++ b/lang/how_to_use_metrics.md @@ -70,24 +70,28 @@ some_counter.inc({"GET", "/"}, 1); ## counter/gauge指标的api 构造函数: + ```cpp -// 无标签,调用inc时不带标签,如c.inc() +// 无标签,调用inc时不带标签,如c.inc(),调用此函数则metric 为静态标签的metric // name: 指标对象的名称,注册到指标管理器时会使用这个名称 // help: 指标对象的帮助信息 counter_t(std::string name, std::string help); // labels: 静态标签,构造时需要将标签键值都填完整,如:{{"method", "GET"}, {"url", "/"}} +// 调用此函数则metric 为静态标签的metric // 调用inc时必须带静态标签的值,如:c.inc({"GET", "/"}, 1); counter_t(std::string name, std::string help, std::map labels); // labels_name: 动态标签的键名称,因为标签的值是动态的,而键的名称是固定的,所以这里只需要填键名称,如: {"method", "url"} // 调用时inc时必须带动态标签的值,如:c.inc({method, url}, 1); +// 调用此函数则metric 为动态标签的metric counter_t(std::string name, std::string help, std::vector labels_name); ``` 基本函数: + ```cpp // 获取无标签指标的计数, double value(); @@ -105,14 +109,14 @@ void inc(const std::vector &labels_value, double value = 1); void serialize(std::string &str); // 返回带标签的指标内部的计数map,map的key是标签的值,值是对应计数,如:{{{"GET", "/"}, 100}, {{"POST", "/test"}, 20}} -std::map, double, - std::less>> +std::unordered_map, double, vector_hash> value_map(); ``` 注意:如果使用动态标签的时候要注意这个动态的标签值是不是无限多的,如果是无限多的话,那么内部的map也会无限增长,应该避免这种情况,动态的标签也应该是有限的才对。 gauge 派生于counter,相比counter多了一个减少计数的api + ```cpp // 无标签指标减少计数 void dec(double value = 1); @@ -151,12 +155,18 @@ class metric_t { // 获取标签的键,如{"method", "url"} const std::vector& labels_name(); + // 获取静态标签,如{{"method", "GET"}, {"code", "200"}} + const std::map& get_static_labels(); + // 序列化,调用派生类实现序列化 virtual void serialize(std::string& str); // 给summary专用的api,序列化,调用派生类实现序列化 virtual async_simple::coro::Lazy serialize_async(std::string& out); + // 序列化到json + void serialize_to_json(std::string& str); + // 将基类指针向下转换到派生类指针,如: // std::shared_ptr c = std::make_shared("test", "test"); // counter_t* t = c->as(); @@ -188,6 +198,7 @@ CHECK(m1->as()->value() == 1); ``` 如果希望动态注册的到管理器则应该调用register_metric_dynamic接口,后面根据名称获取指标对象时则调用get_metric_dynamic接口,dynamic接口内部会加锁。 + ```cpp auto c = std::make_shared("qps_count", "qps help"); auto g = std::make_shared("fd_count", "fd count help"); @@ -222,6 +233,10 @@ struct metric_manager_t { static bool register_metric_static(std::shared_ptr metric); static bool register_metric_dynamic(std::shared_ptr metric); + // 根据metric名称删除metric + static bool remove_metric_static(const std::string& name); + static bool remove_metric_dynamic(const std::string& name); + // 获取注册的所有指标对象 static std::map> metric_map_static(); static std::map> metric_map_dynamic(); @@ -244,9 +259,43 @@ struct metric_manager_t { static std::shared_ptr get_metric_static(const std::string& name); static std::shared_ptr get_metric_dynamic(const std::string& name); + // 根据静态标签获取所有的指标, 如{{"method", "GET"}, {"url", "/"}} + static std::vector> get_metric_by_labels_static( + const std::map& labels); + + // 根据标签值获取所有的静态标签的指标, 如{"method", "GET"} + static std::vector> get_metric_by_label_static( + const std::pair& label); + + // 根据标签值获取所有动态标签的指标, 如{"method", "GET"} + static std::vector> get_metric_by_labels_dynamic( + const std::map& labels); + // 序列化 static async_simple::coro::Lazy serialize_static(); static async_simple::coro::Lazy serialize_dynamic(); + + // 序列化静态标签的指标到json + static std::string serialize_to_json_static(); + // 序列化动态标签的指标到json + static std::string serialize_to_json_dynamic(); + // 序列化metric集合到json + static std::string serialize_to_json( + const std::vector>& metrics); + + // 过滤配置选项,如果name_regex和label_regex都设置了,则会检查这两个条件,如果只设置了一个则只检查设置过的条件 + struct metric_filter_options { + std::optional name_regex{}; // metric 名称的过滤正则表达式 + std::optional label_regex{};// metric label名称的过滤正则表达式 + bool is_white = true; //true: 白名单,包括语义;false: 黑名单,排除语义 + }; + + // 过滤静态标签的指标 + static std::vector> filter_metrics_static( + const metric_filter_options& options); + // 过滤动态标签的指标 + static std::vector> filter_metrics_dynamic( + const metric_filter_options& options); }; using default_metric_manager = metric_manager_t<0>; ``` @@ -267,11 +316,23 @@ using my_metric_manager = metric_manager_t; // 内部还有一个+Inf 默认的桶,当输入的数据不在前面设置这些桶中,则会落到+Inf 默认桶中。 // 实际上桶的总数为 buckets.size() + 1 // 每个bucket 实际上对应了一个counter指标 +// 调用此函数,则metric为静态metric指标 histogram_t(std::string name, std::string help, std::vector buckets); +// labels_value: 标签key,后面可以使用动态标签值去observe,调用此函数则metric为动态metric 指标 +histogram_t(std::string name, std::string help, std::vector buckets, + std::vector labels_name); + +// labels: 静态标签,调用此函数则metric为静态metric指标 +histogram_t(std::string name, std::string help, std::vector buckets, + std::map labels); + // 往histogram_t 中插入数据,内部会自动增加对应桶的计数 void observe(double value); +// 根据标签值插入数据,可以是动态标签值也可以是静态标签值。如果是静态标签,会做额外的检车,检查传入的labels_value是否和注册时的静态标签值是否相同,不相同会抛异常; +void observe(const std::vector &labels_value, double value); + // 获取所有桶对应的counter指标对象 std::vector> get_bucket_counts(); @@ -301,18 +362,63 @@ void serialize(std::string& str); CHECK(str.find("test_bucket{le=\"+Inf\"}") != std::string::npos); ``` +创建Histogram时需要指定桶(bucket),采样点统计数据会落到不同的桶中,并且还需要统计采样点数据的累计总和(sum)以及次数的总和(count)。注意bucket 列表必须是有序的,否则构造时会抛异常。 + +Histogram统计的特点是:数据是累积的,比如由10, 100,两个桶,第一个桶的数据是所有值 <= 10的样本数据存在桶中,第二个桶是所有 <=100 的样本数据存在桶中,其它数据则存放在`+Inf`的桶中。 + +```cpp + auto h = std::make_shared( + std::string("test"), std::string("help"), std::vector{10.0, 100.0}); + metric_t::regiter_metric(h); + + h->observe(5); + h->observe(80); + h->observe(120); + + std::string str; + h.serialize(str); + std::cout< labels_name); + +// static_labels:静态标签,调用此函数则metric为静态metric 指标 +summary_t(std::string name, std::string help, Quantiles quantiles, std::map static_labels); + // 往summary_t插入数据,会自动计算百分位的数量 void observe(double value); -// 获取百分位结果 -async_simple::coro::Lazy> get_rates(); +// 根据标签值(动态或静态的标签值,依据构造函数决定是动态还是静态metric),往summary_t插入数据,会自动计算百分位的数量 +void observe(std::vector labels_value, double value); + +// 获取分位数结果, sum 和count +async_simple::coro::Lazy> get_rates(double &sum, + uint64_t &count) +// 根据标签获取分位数结果, sum 和count +async_simple::coro::Lazy> get_rates( + const std::vector &labels_value, double &sum, + uint64_t &count); // 获取总和 async_simple::coro::Lazy get_sum(); @@ -349,6 +455,45 @@ async_simple::coro::Lazy serialize_async(std::string &str); ``` summary 百分位的计算相比其它指标是最耗时的,应该避免在关键路径上使用它以免对性能造成影响。 +创建Summary时需要指定分位数和误差,分位数在0到1之间,左右都为闭区间,比如p50就是一个中位数,p99指中位数为0.99的分位数。 +```cpp + summary_t summary{"test_summary", + "summary help", + {{0.5, 0.05}, {0.9, 0.01}, {0.95, 0.005}, {0.99, 0.001}}}; + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> distr(1, 100); + for (int i = 0; i < 50; i++) { + summary.observe(distr(gen)); + } + + std::string str; + summary.serialize(str); + std::cout << str; +``` +输出: +``` +# HELP test_summary summary help +# TYPE test_summary summary +test_summary{quantile="0.500000"} 45.000000 +test_summary{quantile="0.900000"} 83.000000 +test_summary{quantile="0.950000"} 88.000000 +test_summary{quantile="0.990000"} 93.000000 +test_summary_sum 2497.000000 +test_summary_count 50 +``` + +## 配置prometheus 前端 +安装[prometheus](https://github.com/prometheus/prometheus)之后,打开其配置文件:prometheus.yml + +修改要连接的服务端地址: +``` +- targets: ["127.0.0.1:9001"] +``` +然后启动prometheus,prometheus会定时访问`http://127.0.0.1:9001/metrics` 拉取所有指标数据。 + +在本地浏览器输入:127.0.0.1:9090, 打开prometheus前端,在前端页面的搜索框中输入指标的名称request_count之后就能看到table和graph 结果了。 + # cinatra http server中启用内置的metric指标 http server 内置的指标: diff --git a/press_tool/CMakeLists.txt b/press_tool/CMakeLists.txt index 4db1f8c4..0810829d 100644 --- a/press_tool/CMakeLists.txt +++ b/press_tool/CMakeLists.txt @@ -8,7 +8,9 @@ set(cinatra_press_tool ) include_directories(../include) -include_directories(../include/cinatra) +if(ENABLE_METRIC_JSON) + include_directories(../iguana) +endif() add_definitions(-DBENCHMARK_TEST) add_executable(${project_name} ${cinatra_press_tool}) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cdd6eb12..d9962e4d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,6 +12,11 @@ target_include_directories(${project_name} PRIVATE ${cinatra_SOURCE_DIR}/include ${cinatra_SOURCE_DIR}/include/cinatra ) +if(ENABLE_METRIC_JSON) + target_include_directories(${project_name} PRIVATE + ${cinatra_SOURCE_DIR}/iguana + ) +endif() option(SKIP_TIME_TEST "skip time tests" OFF) if(SKIP_TIME_TEST) @@ -51,7 +56,7 @@ endif() ## manual import include_directories(${cinatra_SOURCE_DIR}/include) -include_directories(${cinatra_SOURCE_DIR}/include/cinatra) +include_directories(${cinatra_SOURCE_DIR}/iguana) add_executable(test_corofile test_corofile.cpp diff --git a/tests/test_metric.cpp b/tests/test_metric.cpp index d5014605..86a6039d 100644 --- a/tests/test_metric.cpp +++ b/tests/test_metric.cpp @@ -11,6 +11,7 @@ #include "cinatra/ylt/metric/summary.hpp" #include "doctest/doctest.h" using namespace ylt; +using namespace ylt::metric; TEST_CASE("test no lable") { { @@ -143,6 +144,13 @@ TEST_CASE("test gauge") { CHECK(g.value() == 2); g.inc(0); +#ifdef CINATRA_ENABLE_METRIC_JSON + std::string str_json; + g.serialize_to_json(str_json); + std::cout << str_json << "\n"; + CHECK(str_json.find("\"value\":2") != std::string::npos); +#endif + g.dec(); CHECK(g.value() == 1); g.dec(); @@ -160,6 +168,15 @@ TEST_CASE("test gauge") { values = g.value_map(); CHECK(values[{"GET", "200", "/"}] == 3); + g.inc({"POST", "200", "/"}, 4); + +#ifdef CINATRA_ENABLE_METRIC_JSON + std::string str_json; + g.serialize_to_json(str_json); + std::cout << str_json << "\n"; + CHECK(str_json.find("\"code\":\"200\"") != std::string::npos); +#endif + std::string str; g.serialize(str); std::cout << str; @@ -179,7 +196,7 @@ TEST_CASE("test gauge") { } TEST_CASE("test histogram") { - histogram_t h("test", "help", {5.0, 10.0, 20.0, 50.0, 100.0}); + histogram_t h("test", "help", {5.23, 10.54, 20.0, 50.0, 100.0}); h.observe(23); auto counts = h.get_bucket_counts(); CHECK(counts[3]->value() == 1); @@ -193,11 +210,18 @@ TEST_CASE("test histogram") { CHECK(counts[0]->value() == 1); std::string str; h.serialize(str); - std::cout << str; + std::cout << str << "\n"; CHECK(str.find("test_count") != std::string::npos); CHECK(str.find("test_sum") != std::string::npos); - CHECK(str.find("test_bucket{le=\"5") != std::string::npos); + CHECK(str.find("test_bucket{le=\"5.23") != std::string::npos); CHECK(str.find("test_bucket{le=\"+Inf\"}") != std::string::npos); + +#ifdef CINATRA_ENABLE_METRIC_JSON + std::string str_json; + h.serialize_to_json(str_json); + std::cout << str_json << "\n"; + CHECK(str_json.find("\"5.23\":1") != std::string::npos); +#endif } TEST_CASE("test summary") { @@ -221,6 +245,13 @@ TEST_CASE("test summary") { CHECK(str.find("test_summary_count") != std::string::npos); CHECK(str.find("test_summary_sum") != std::string::npos); CHECK(str.find("test_summary{quantile=\"") != std::string::npos); + +#ifdef CINATRA_ENABLE_METRIC_JSON + std::string str_json; + async_simple::coro::syncAwait(summary.serialize_to_json_async(str_json)); + std::cout << str_json << "\n"; + CHECK(str_json.find("\"0.9\":") != std::string::npos); +#endif } TEST_CASE("test register metric") { @@ -249,8 +280,7 @@ TEST_CASE("test register metric") { CHECK(map["get_count"]->as()->value() == 1); CHECK(map["get_guage_count"]->as()->value() == 1); - auto s = - async_simple::coro::syncAwait(default_metric_manager::serialize_static()); + auto s = default_metric_manager::serialize_static(); std::cout << s << "\n"; CHECK(s.find("get_count 1") != std::string::npos); CHECK(s.find("get_guage_count 1") != std::string::npos); @@ -279,6 +309,474 @@ TEST_CASE("test register metric") { } } +TEST_CASE("test remove metric and serialize metrics") { + using metric_mgr = metric_manager_t<1>; + metric_mgr::create_metric_dynamic("test_counter", ""); + metric_mgr::create_metric_dynamic("test_counter2", ""); + + size_t count = metric_mgr::metric_count_dynamic(); + CHECK(count == 2); + + metric_mgr::remove_metric_dynamic("test_counter"); + count = metric_mgr::metric_count_dynamic(); + CHECK(count == 1); + + metric_mgr::remove_metric_dynamic("test_counter2"); + count = metric_mgr::metric_count_dynamic(); + CHECK(count == 0); + + CHECK_THROWS_AS( + metric_mgr::create_metric_static("test_static_counter", ""), + std::invalid_argument); + + using metric_mgr2 = metric_manager_t<2>; + auto c = + metric_mgr2::create_metric_static("test_static_counter", ""); + auto c2 = + metric_mgr2::create_metric_static("test_static_counter2", ""); + c->inc(); + c2->inc(); + +#ifdef CINATRA_ENABLE_METRIC_JSON + auto s = metric_mgr2::serialize_to_json_static(); + std::cout << s << "\n"; + auto s1 = metric_mgr2::serialize_to_json({c, c2}); + CHECK(s == s1); +#endif + CHECK_THROWS_AS(metric_mgr2::metric_count_dynamic(), std::invalid_argument); + count = metric_mgr2::metric_count_static(); + CHECK(count == 2); + CHECK_THROWS_AS(metric_mgr2::remove_metric_dynamic("test_static_counter"), + std::invalid_argument); + + metric_mgr2::remove_metric_static("test_static_counter"); + count = metric_mgr2::metric_count_static(); + CHECK(count == 1); +} + +TEST_CASE("test filter metrics static") { + using metric_mgr = metric_manager_t<3>; + auto c = metric_mgr::create_metric_static( + "test_static_counter", "", + std::map{{"method", "GET"}}); + auto c2 = metric_mgr::create_metric_static( + "test_static_counter2", "", + std::map{{"url", "/"}}); + c->inc({"GET"}); + c2->inc({"/"}); + + metric_filter_options options; + options.name_regex = ".*counter.*"; + { + auto metrics = metric_mgr::filter_metrics_static(options); + CHECK(metrics.size() == 2); + + auto s = metric_mgr::serialize(metrics); + CHECK(s.find("test_static_counter") != std::string::npos); + std::cout << s << "\n"; + } + + options.label_regex = ".*ur.*"; + { + auto metrics = metric_mgr::filter_metrics_static(options); + CHECK(metrics.size() == 1); + auto s = metric_mgr::serialize(metrics); + CHECK(s.find("test_static_counter2") != std::string::npos); + std::cout << s << "\n"; + } + + options.name_regex = "no_such_name"; + { + auto metrics = metric_mgr::filter_metrics_static(options); + CHECK(metrics.empty()); + auto s = metric_mgr::serialize(metrics); + CHECK(s.empty()); + } + + options = {}; + options.label_regex = "no_such_label"; + { + auto metrics = metric_mgr::filter_metrics_static(options); + CHECK(metrics.empty()); + auto s = metric_mgr::serialize(metrics); + CHECK(s.empty()); + } + + // don't filter + options = {}; + { + auto metrics = metric_mgr::filter_metrics_static(options); + CHECK(metrics.size() == 2); + } + + // black + options.label_regex = ".*ur.*"; + options.is_white = false; + { + auto metrics = metric_mgr::filter_metrics_static(options); + CHECK(metrics.size() == 1); + auto s = metric_mgr::serialize(metrics); + CHECK(s.find("test_static_counter") != std::string::npos); + CHECK(s.find("test_static_counter2") == std::string::npos); + } + + options = {}; + options.label_regex = ".*ur.*"; + options.is_white = false; + { + auto metrics = metric_mgr::filter_metrics_static(options); + CHECK(metrics.size() == 1); + auto s = metric_mgr::serialize(metrics); + CHECK(s.find("test_static_counter") != std::string::npos); + CHECK(s.find("method") != std::string::npos); + CHECK(s.find("test_static_counter2") == std::string::npos); + CHECK(s.find("url") == std::string::npos); + } +} + +TEST_CASE("test filter metrics dynamic") { + using metric_mgr = metric_manager_t<4>; + auto c = metric_mgr::create_metric_dynamic( + "test_dynamic_counter", "", std::vector{{"method"}}); + auto c2 = metric_mgr::create_metric_dynamic( + "test_dynamic_counter2", "", std::vector{{"url"}}); + c->inc({"GET"}); + c->inc({"POST"}); + c2->inc({"/"}); + c2->inc({"/test"}); + + metric_filter_options options; + options.name_regex = ".*counter.*"; + { + auto metrics = metric_mgr::filter_metrics_dynamic(options); + CHECK(metrics.size() == 2); + + auto s = metric_mgr::serialize(metrics); + CHECK(s.find("test_dynamic_counter") != std::string::npos); + std::cout << s << "\n"; + } + + options.label_regex = ".*ur.*"; + { + auto metrics = metric_mgr::filter_metrics_dynamic(options); + CHECK(metrics.size() == 1); + auto s = metric_mgr::serialize(metrics); + CHECK(s.find("test_dynamic_counter2") != std::string::npos); + std::cout << s << "\n"; + } + + options.name_regex = "no_such_name"; + { + auto metrics = metric_mgr::filter_metrics_dynamic(options); + CHECK(metrics.empty()); + auto s = metric_mgr::serialize(metrics); + CHECK(s.empty()); + } + + options = {}; + options.label_regex = "no_such_label"; + { + auto metrics = metric_mgr::filter_metrics_dynamic(options); + CHECK(metrics.empty()); + auto s = metric_mgr::serialize(metrics); + CHECK(s.empty()); + } + + // don't filter + options = {}; + { + auto metrics = metric_mgr::filter_metrics_dynamic(options); + CHECK(metrics.size() == 2); + } + + // black + options.label_regex = ".*ur.*"; + options.is_white = false; + { + auto metrics = metric_mgr::filter_metrics_dynamic(options); + CHECK(metrics.size() == 1); + auto s = metric_mgr::serialize(metrics); + CHECK(s.find("test_dynamic_counter") != std::string::npos); + CHECK(s.find("test_dynamic_counter2") == std::string::npos); + } + + options = {}; + options.label_regex = ".*ur.*"; + options.is_white = false; + { + auto metrics = metric_mgr::filter_metrics_dynamic(options); + CHECK(metrics.size() == 1); + auto s = metric_mgr::serialize(metrics); + CHECK(s.find("test_dynamic_counter") != std::string::npos); + CHECK(s.find("method") != std::string::npos); + CHECK(s.find("test_dynamic_counter2") == std::string::npos); + CHECK(s.find("url") == std::string::npos); + } +} + +TEST_CASE("test get metric by static labels and label") { + using metric_mgr = metric_manager_t<9>; + metric_mgr::create_metric_static( + "http_req_test", "", + std::map{{"method", "GET"}, {"url", "/"}}); + metric_mgr::create_metric_static( + "http_req_test1", "", + std::map{{"method", "POST"}, {"url", "/"}}); + metric_mgr::create_metric_static( + "http_req_test2", "", + std::map{{"method", "GET"}, {"url", "/test"}}); + + auto v = metric_mgr::get_metric_by_labels_static( + std::map{{"method", "GET"}, {"url", "/test"}}); + CHECK(v[0]->name() == "http_req_test2"); + + v = metric_mgr::get_metric_by_labels_static( + std::map{{"method", "GET"}, {"url", "/"}}); + CHECK(v[0]->name() == "http_req_test"); + + auto h1 = metric_mgr::create_metric_static( + "http_req_static_hist", "help", + std::vector{5.23, 10.54, 20.0, 50.0, 100.0}, + std::map{{"method", "GET"}, {"url", "/"}}); + + h1->observe({"GET", "/"}, 23); + + auto s1 = metric_mgr::create_metric_static( + "http_req_static_summary", "help", + summary_t::Quantiles{ + {0.5, 0.05}, {0.9, 0.01}, {0.95, 0.005}, {0.99, 0.001}}, + std::map{{"method", "GET"}, {"url", "/"}}); + s1->observe({"GET", "/"}, 23); + + auto vec = metric_mgr::get_metric_by_label_static({"method", "GET"}); + CHECK(vec.size() == 4); + + vec = metric_mgr::get_metric_by_label_static({"url", "/"}); + CHECK(vec.size() == 4); + + vec = metric_mgr::get_metric_by_label_static({"url", "/test"}); + CHECK(vec.size() == 1); + + vec = metric_mgr::get_metric_by_label_static({"method", "POST"}); + CHECK(vec.size() == 1); + + vec = metric_mgr::get_metric_by_labels_static( + std::map{{"method", "HEAD"}, {"url", "/test"}}); + CHECK(vec.empty()); + + vec = metric_mgr::get_metric_by_labels_static( + std::map{{"method", "GET"}}); + CHECK(vec.empty()); + + vec = metric_mgr::get_metric_by_label_static({"url", "/index"}); + CHECK(vec.empty()); +} + +TEST_CASE("test get metric by dynamic labels") { + using metric_mgr = metric_manager_t<10>; + auto c = metric_mgr::create_metric_dynamic( + "http_req_static", "", std::vector{"method", "code"}); + + auto c1 = metric_mgr::create_metric_dynamic( + "http_req_static1", "", std::vector{"method", "code"}); + + auto c2 = metric_mgr::create_metric_dynamic( + "http_req_static2", "", std::vector{"method", "code"}); + + auto c3 = metric_mgr::create_metric_dynamic( + "http_req_static3", "", std::vector{"method", "code"}); + + c->inc({"POST", "200"}); + c1->inc({"GET", "200"}); + c2->inc({"POST", "301"}); + c3->inc({"POST", "400"}); + + auto c4 = metric_mgr::create_metric_dynamic( + "http_req_static4", "", std::vector{"host", "url"}); + + auto c5 = metric_mgr::create_metric_dynamic( + "http_req_static5", "", std::vector{"host", "url"}); + + c4->inc({"shanghai", "/"}); + c5->inc({"shanghai", "/test"}); + + auto vec = metric_mgr::get_metric_by_labels_dynamic({{"method", "POST"}}); + CHECK(vec.size() == 3); + + vec = metric_mgr::get_metric_by_labels_dynamic({{"method", "GET"}}); + CHECK(vec.size() == 1); + + vec = metric_mgr::get_metric_by_labels_dynamic({{"host", "shanghai"}}); + CHECK(vec.size() == 2); + + vec = metric_mgr::get_metric_by_labels_dynamic({{"url", "/"}}); + CHECK(vec.size() == 1); + + vec = metric_mgr::get_metric_by_labels_dynamic({{"url", "/test"}}); + CHECK(vec.size() == 1); + + vec = metric_mgr::get_metric_by_labels_dynamic({{"url", "/none"}}); + CHECK(vec.size() == 0); + + vec = metric_mgr::get_metric_by_labels_dynamic({{"method", "HEAD"}}); + CHECK(vec.size() == 0); + + auto h1 = metric_mgr::create_metric_dynamic( + "http_req_static_hist", "help", + std::vector{5.23, 10.54, 20.0, 50.0, 100.0}, + std::vector{"method", "url"}); + + h1->observe({"GET", "/"}, 23); + + auto s1 = metric_mgr::create_metric_dynamic( + "http_req_static_summary", "help", + summary_t::Quantiles{ + {0.5, 0.05}, {0.9, 0.01}, {0.95, 0.005}, {0.99, 0.001}}, + std::vector{"method", "url"}); + s1->observe({"GET", "/"}, 23); + + vec = metric_mgr::get_metric_by_labels_dynamic({{"method", "GET"}}); + CHECK(vec.size() >= 2); + + auto str = metric_mgr::serialize(vec); + std::cout << str; + +#ifdef CINATRA_ENABLE_METRIC_JSON + auto json_str = metric_mgr::serialize_to_json(vec); + std::cout << json_str << "\n"; +#endif +} + +TEST_CASE("test histogram serialize with dynamic labels") { + histogram_t h("test", "help", {5.23, 10.54, 20.0, 50.0, 100.0}, + std::vector{"method", "url"}); + h.observe({"GET", "/"}, 23); + auto counts = h.get_bucket_counts(); + CHECK(counts[3]->value({"GET", "/"}) == 1); + h.observe({"GET", "/"}, 42); + CHECK(counts[3]->value({"GET", "/"}) == 2); + h.observe({"GET", "/"}, 60); + CHECK(counts[4]->value({"GET", "/"}) == 1); + h.observe({"GET", "/"}, 120); + CHECK(counts[5]->value({"GET", "/"}) == 1); + h.observe({"GET", "/"}, 1); + CHECK(counts[0]->value({"GET", "/"}) == 1); + + h.observe({"POST", "/"}, 23); + CHECK(counts[3]->value({"POST", "/"}) == 1); + h.observe({"POST", "/"}, 42); + CHECK(counts[3]->value({"POST", "/"}) == 2); + h.observe({"POST", "/"}, 60); + CHECK(counts[4]->value({"POST", "/"}) == 1); + h.observe({"POST", "/"}, 120); + CHECK(counts[5]->value({"POST", "/"}) == 1); + h.observe({"POST", "/"}, 1); + CHECK(counts[0]->value({"POST", "/"}) == 1); + + std::string str; + h.serialize(str); + std::cout << str; + +#ifdef CINATRA_ENABLE_METRIC_JSON + std::string str_json; + h.serialize_to_json(str_json); + std::cout << str_json << "\n"; +#endif +} + +TEST_CASE("test histogram serialize with static labels") { + histogram_t h( + "test", "help", {5.23, 10.54, 20.0, 50.0, 100.0}, + std::map{{"method", "GET"}, {"url", "/"}}); + h.observe({"GET", "/"}, 23); + auto counts = h.get_bucket_counts(); + CHECK(counts[3]->value({"GET", "/"}) == 1); + h.observe({"GET", "/"}, 42); + CHECK(counts[3]->value({"GET", "/"}) == 2); + h.observe({"GET", "/"}, 60); + CHECK(counts[4]->value({"GET", "/"}) == 1); + h.observe({"GET", "/"}, 120); + CHECK(counts[5]->value({"GET", "/"}) == 1); + h.observe({"GET", "/"}, 1); + CHECK(counts[0]->value({"GET", "/"}) == 1); + + std::string str; + h.serialize(str); + std::cout << str; + +#ifdef CINATRA_ENABLE_METRIC_JSON + std::string str_json; + h.serialize_to_json(str_json); + std::cout << str_json << "\n"; +#endif +} + +TEST_CASE("test summary with dynamic labels") { + summary_t summary{"test_summary", + "summary help", + {{0.5, 0.05}, {0.9, 0.01}, {0.95, 0.005}, {0.99, 0.001}}, + std::vector{"method", "url"}}; + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> distr(1, 100); + for (int i = 0; i < 50; i++) { + summary.observe({"GET", "/"}, distr(gen)); + summary.observe({"POST", "/test"}, distr(gen)); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + double sum; + uint64_t count; + auto rates = async_simple::coro::syncAwait( + summary.get_rates({"GET", "/"}, sum, count)); + std::cout << rates.size() << "\n"; + + std::string str; + async_simple::coro::syncAwait(summary.serialize_async(str)); + std::cout << str; + +#ifdef CINATRA_ENABLE_METRIC_JSON + std::string json_str; + async_simple::coro::syncAwait(summary.serialize_to_json_async(json_str)); + std::cout << json_str << "\n"; +#endif +} + +TEST_CASE("test summary with static labels") { + summary_t summary{ + "test_summary", + "summary help", + {{0.5, 0.05}, {0.9, 0.01}, {0.95, 0.005}, {0.99, 0.001}}, + std::map{{"method", "GET"}, {"url", "/"}}}; + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> distr(1, 100); + for (int i = 0; i < 50; i++) { + summary.observe({"GET", "/"}, distr(gen)); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + CHECK_THROWS_AS(summary.observe({"POST", "/"}, 1), std::invalid_argument); + + double sum; + uint64_t count; + auto rates = async_simple::coro::syncAwait( + summary.get_rates({"GET", "/"}, sum, count)); + std::cout << rates.size() << "\n"; + + std::string str; + async_simple::coro::syncAwait(summary.serialize_async(str)); + std::cout << str; + +#ifdef CINATRA_ENABLE_METRIC_JSON + std::string json_str; + async_simple::coro::syncAwait(summary.serialize_to_json_async(json_str)); + std::cout << json_str << "\n"; +#endif +} + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) int main(int argc, char** argv) { return doctest::Context(argc, argv).run(); } DOCTEST_MSVC_SUPPRESS_WARNING_POP \ No newline at end of file