From cedeeb59f4704fd025dbc81eea694e3836c480b4 Mon Sep 17 00:00:00 2001 From: An Tao Date: Mon, 4 Sep 2023 10:16:44 +0800 Subject: [PATCH] Add a plugin for prometheus (#1632) --- CMakeLists.txt | 52 +++-- config.example.json | 33 ++- lib/inc/drogon/drogon.h | 1 + lib/inc/drogon/plugins/PromExporter.h | 98 +++++++++ lib/inc/drogon/utils/monitoring.h | 18 ++ lib/inc/drogon/utils/monitoring/Collector.h | 132 +++++++++++ lib/inc/drogon/utils/monitoring/Counter.h | 82 +++++++ lib/inc/drogon/utils/monitoring/Gauge.h | 109 +++++++++ lib/inc/drogon/utils/monitoring/Histogram.h | 122 +++++++++++ lib/inc/drogon/utils/monitoring/Metric.h | 77 +++++++ lib/inc/drogon/utils/monitoring/Registry.h | 35 +++ lib/inc/drogon/utils/monitoring/Sample.h | 35 +++ lib/inc/drogon/utils/monitoring/StopWatch.h | 75 +++++++ lib/src/Histogram.cc | 80 +++++++ lib/src/PromExporter.cc | 207 ++++++++++++++++++ .../integration_test/server/TestController.cc | 1 + .../integration_test/server/TestController.h | 12 + 17 files changed, 1134 insertions(+), 35 deletions(-) create mode 100644 lib/inc/drogon/plugins/PromExporter.h create mode 100644 lib/inc/drogon/utils/monitoring.h create mode 100644 lib/inc/drogon/utils/monitoring/Collector.h create mode 100644 lib/inc/drogon/utils/monitoring/Counter.h create mode 100644 lib/inc/drogon/utils/monitoring/Gauge.h create mode 100644 lib/inc/drogon/utils/monitoring/Histogram.h create mode 100644 lib/inc/drogon/utils/monitoring/Metric.h create mode 100644 lib/inc/drogon/utils/monitoring/Registry.h create mode 100644 lib/inc/drogon/utils/monitoring/Sample.h create mode 100644 lib/inc/drogon/utils/monitoring/StopWatch.h create mode 100644 lib/src/Histogram.cc create mode 100644 lib/src/PromExporter.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e93ac5a31..8802602541 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -230,12 +230,18 @@ endif (BUILD_BROTLI) set(DROGON_SOURCES lib/src/AOPAdvice.cc + lib/src/AccessLogger.cc lib/src/CacheFile.cc + lib/src/ConfigAdapterManager.cc lib/src/ConfigLoader.cc lib/src/Cookie.cc lib/src/DrClassMap.cc lib/src/DrTemplateBase.cc lib/src/FiltersFunction.cc + lib/src/FixedWindowRateLimiter.cc + lib/src/GlobalFilters.cc + lib/src/Histogram.cc + lib/src/Hodor.cc lib/src/HttpAppFrameworkImpl.cc lib/src/HttpBinder.cc lib/src/HttpClientImpl.cc @@ -251,33 +257,29 @@ set(DROGON_SOURCES lib/src/HttpUtils.cc lib/src/HttpViewData.cc lib/src/IntranetIpFilter.cc + lib/src/JsonConfigAdapter.cc lib/src/ListenerManager.cc lib/src/LocalHostFilter.cc lib/src/MultiPart.cc lib/src/NotFound.cc lib/src/PluginsManager.cc + lib/src/PromExporter.cc lib/src/RangeParser.cc - lib/src/SecureSSLRedirector.cc - lib/src/GlobalFilters.cc - lib/src/AccessLogger.cc + lib/src/RateLimiter.cc lib/src/RealIpResolver.cc + lib/src/SecureSSLRedirector.cc lib/src/SessionManager.cc + lib/src/SlashRemover.cc + lib/src/SlidingWindowRateLimiter.cc lib/src/StaticFileRouter.cc lib/src/TaskTimeoutFlag.cc + lib/src/TokenBucketRateLimiter.cc lib/src/Utilities.cc lib/src/WebSocketClientImpl.cc lib/src/WebSocketConnectionImpl.cc lib/src/WebsocketControllersRouter.cc - lib/src/RateLimiter.cc - lib/src/FixedWindowRateLimiter.cc - lib/src/SlidingWindowRateLimiter.cc - lib/src/TokenBucketRateLimiter.cc - lib/src/Hodor.cc - lib/src/SlashRemover.cc - lib/src/drogon_test.cc - lib/src/ConfigAdapterManager.cc - lib/src/JsonConfigAdapter.cc - lib/src/YamlConfigAdapter.cc) + lib/src/YamlConfigAdapter.cc + lib/src/drogon_test.cc) set(private_headers lib/src/AOPAdvice.h lib/src/CacheFile.h @@ -689,10 +691,23 @@ set(DROGON_UTIL_HEADERS lib/inc/drogon/utils/FunctionTraits.h lib/inc/drogon/utils/HttpConstraint.h lib/inc/drogon/utils/OStringStream.h - lib/inc/drogon/utils/Utilities.h) + lib/inc/drogon/utils/Utilities.h + lib/inc/drogon/utils/monitoring.h) install(FILES ${DROGON_UTIL_HEADERS} DESTINATION ${INSTALL_INCLUDE_DIR}/drogon/utils) +set(DROGON_MONITORING_HEADERS + lib/inc/drogon/utils/monitoring/Counter.h + lib/inc/drogon/utils/monitoring/Metric.h + lib/inc/drogon/utils/monitoring/Registry.h + lib/inc/drogon/utils/monitoring/Collector.h + lib/inc/drogon/utils/monitoring/Sample.h + lib/inc/drogon/utils/monitoring/Gauge.h + lib/inc/drogon/utils/monitoring/Histogram.h) + +install(FILES ${DROGON_MONITORING_HEADERS} + DESTINATION ${INSTALL_INCLUDE_DIR}/drogon/utils/monitoring) + set(DROGON_PLUGIN_HEADERS lib/inc/drogon/plugins/Plugin.h lib/inc/drogon/plugins/SecureSSLRedirector.h @@ -700,7 +715,8 @@ set(DROGON_PLUGIN_HEADERS lib/inc/drogon/plugins/RealIpResolver.h lib/inc/drogon/plugins/Hodor.h lib/inc/drogon/plugins/SlashRemover.h - lib/inc/drogon/plugins/GlobalFilters.h) + lib/inc/drogon/plugins/GlobalFilters.h + lib/inc/drogon/plugins/PromExporter.h) install(FILES ${DROGON_PLUGIN_HEADERS} DESTINATION ${INSTALL_INCLUDE_DIR}/drogon/plugins) @@ -712,7 +728,8 @@ target_sources(${PROJECT_NAME} PRIVATE ${ORM_HEADERS} ${DROGON_UTIL_HEADERS} ${DROGON_PLUGIN_HEADERS} - ${NOSQL_HEADERS}) + ${NOSQL_HEADERS} + ${DROGON_MONITORING_HEADERS}) source_group("Public API" FILES @@ -720,7 +737,8 @@ source_group("Public API" ${ORM_HEADERS} ${DROGON_UTIL_HEADERS} ${DROGON_PLUGIN_HEADERS} - ${NOSQL_HEADERS}) + ${NOSQL_HEADERS} + ${DROGON_MONITORING_HEADERS}) source_group("Private Headers" FILES ${private_headers}) diff --git a/config.example.json b/config.example.json index 85e902b601..21c1008f3c 100644 --- a/config.example.json +++ b/config.example.json @@ -244,19 +244,19 @@ //0 means cache forever, the negative value means no cache "static_files_cache_time": 5, //simple_controllers_map: Used to configure mapping from path to simple controller - "simple_controllers_map": [ - { - "path": "/path/name", - "controller": "controllerClassName", - "http_methods": [ - "get", - "post" - ], - "filters": [ - "FilterClassName" - ] - } - ], + //"simple_controllers_map": [ + // { + // "path": "/path/name", + // "controller": "controllerClassName", + // "http_methods": [ + // "get", + // "post" + // ], + // "filters": [ + // "FilterClassName" + // ] + // } + //], //idle_connection_timeout: Defaults to 60 seconds, the lifetime //of the connection without read or write "idle_connection_timeout": 60, @@ -307,16 +307,13 @@ "plugins": [ { //name: The class name of the plugin - //"name": "drogon::plugin::SecureSSLRedirector", + "name": "drogon::plugin::PromExporter", //dependencies: Plugins that the plugin depends on. It can be commented out "dependencies": [], //config: The configuration of the plugin. This json object is the parameter to initialize the plugin. //It can be commented out "config": { - "ssl_redirect_exempt": [ - ".*\\.jpg" - ], - "secure_ssl_host": "localhost:8849" + "path": "/metrics" } } ], diff --git a/lib/inc/drogon/drogon.h b/lib/inc/drogon/drogon.h index 21264c652c..932cb407c9 100644 --- a/lib/inc/drogon/drogon.h +++ b/lib/inc/drogon/drogon.h @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include diff --git a/lib/inc/drogon/plugins/PromExporter.h b/lib/inc/drogon/plugins/PromExporter.h new file mode 100644 index 0000000000..ba2f4f7385 --- /dev/null +++ b/lib/inc/drogon/plugins/PromExporter.h @@ -0,0 +1,98 @@ +/** + * @file PromExporter.h + * @author An Tao + * + * Copyright 2018, An Tao. All rights reserved. + * https://github.com/an-tao/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#pragma once +#include +#include +#include +#include +#include + +namespace drogon +{ +namespace plugin +{ +/** + * @brief The PromExporter plugin implements a prometheus exporter. + * The json configuration is as follows: + * @code + { + "name": "drogon::plugin::PromExporter", + "dependencies": [], + "config": { + // The path of the metrics. the default value is "/metrics". + "path": "/metrics", + // The list of collectors. + "collectors":[ + { + // The name of the collector. + "name": "http_requests_total", + // The help message of the collector. + "help": "The total number of http requests", + // The type of the collector. The default value is "counter". + // The other possible value is as following: + // "gauge", "histogram". + "type": "counter", + // The labels of the collector. + "labels": ["method", "status"] + } + ] + } + } + @endcode + * */ +class DROGON_EXPORT PromExporter + : public drogon::Plugin, + public std::enable_shared_from_this, + public drogon::monitoring::Registry +{ + public: + PromExporter() + { + } + + void initAndStart(const Json::Value &config) override; + + void shutdown() override + { + } + + ~PromExporter() override + { + } + + void registerCollector( + const std::shared_ptr &collector) + override; + + std::shared_ptr getCollector( + const std::string &name) const noexcept(false); + + template + std::shared_ptr> getCollector( + const std::string &name) const + { + return std::dynamic_pointer_cast>( + getCollector(name)); + } + + private: + mutable std::mutex mutex_; + std::unordered_map> + collectors_; + std::string path_{"/metrics"}; + std::string exportMetrics(); +}; +} // namespace plugin +} // namespace drogon diff --git a/lib/inc/drogon/utils/monitoring.h b/lib/inc/drogon/utils/monitoring.h new file mode 100644 index 0000000000..cfb813964f --- /dev/null +++ b/lib/inc/drogon/utils/monitoring.h @@ -0,0 +1,18 @@ +/** + * + * monitoring.h + * An Tao + * + * Copyright 2018, An Tao. All rights reserved. + * https://github.com/an-tao/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ +#pragma once +#include +#include +#include +#include \ No newline at end of file diff --git a/lib/inc/drogon/utils/monitoring/Collector.h b/lib/inc/drogon/utils/monitoring/Collector.h new file mode 100644 index 0000000000..c8293722d4 --- /dev/null +++ b/lib/inc/drogon/utils/monitoring/Collector.h @@ -0,0 +1,132 @@ +/** + * + * Collector.h + * An Tao + * + * Copyright 2018, An Tao. All rights reserved. + * https://github.com/an-tao/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace drogon +{ +namespace monitoring +{ +struct SamplesGroup +{ + std::shared_ptr metric; + std::vector samples; +}; + +class CollectorBase : public std::enable_shared_from_this +{ + public: + virtual ~CollectorBase() = default; + virtual std::vector collect() const = 0; + virtual const std::string &name() const = 0; + virtual const std::string &help() const = 0; + virtual const std::string_view type() const = 0; +}; + +/** + * @brief The Collector class template is used to collect samples from a group + * of metric. + */ +template +class Collector : public CollectorBase +{ + public: + Collector(const std::string &name, + const std::string &help, + const std::vector &labelNames) + : name_(name), help_(help), labelsNames_(labelNames) + { + } + + const std::shared_ptr &metric( + const std::vector &labelValues) noexcept(false) + { + if (labelValues.size() != labelsNames_.size()) + { + throw std::runtime_error( + "The number of label values is not equal to the number of " + "label names!"); + } + std::lock_guard guard(mutex_); + auto iter = metrics_.find(labelValues); + if (iter != metrics_.end()) + { + return iter->second; + } + auto metric = std::make_shared(name_, labelsNames_, labelValues); + metrics_[labelValues] = metric; + return metrics_[labelValues]; + } + + std::vector collect() const override + { + std::lock_guard guard(mutex_); + std::vector samples; + for (auto &pair : metrics_) + { + SamplesGroup samplesGroup; + auto &metric = pair.second; + samplesGroup.metric = metric; + auto metricSamples = metric->collect(); + samplesGroup.samples = std::move(metricSamples); + samples.emplace_back(std::move(samplesGroup)); + } + return samples; + } + + const std::string &name() const override + { + return name_; + } + + const std::string &help() const override + { + return help_; + } + + const std::string_view type() const override + { + return T::type(); + } + + void registerTo(Registry ®istry) + { + registry.registerCollector(shared_from_this()); + } + + const std::vector &labelsNames() const + { + return labelsNames_; + } + + private: + const std::string name_; + const std::string help_; + const std::vector labelsNames_; + std::map, std::shared_ptr> metrics_; + mutable std::mutex mutex_; +}; +} // namespace monitoring +} // namespace drogon \ No newline at end of file diff --git a/lib/inc/drogon/utils/monitoring/Counter.h b/lib/inc/drogon/utils/monitoring/Counter.h new file mode 100644 index 0000000000..17976ffe2f --- /dev/null +++ b/lib/inc/drogon/utils/monitoring/Counter.h @@ -0,0 +1,82 @@ +/** + * + * Counter.h + * An Tao + * + * Copyright 2018, An Tao. All rights reserved. + * https://github.com/an-tao/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#pragma once +#include +#include +#include + +namespace drogon +{ +namespace monitoring +{ +/** + * This class is used to collect samples for a counter metric. + * */ +class Counter : public Metric +{ + public: + Counter(const std::string &name, + const std::vector &labelNames, + const std::vector &labelValues) noexcept(false) + : Metric(name, labelNames, labelValues) + { + } + + std::vector collect() const override + { + Sample s; + s.name = name_; + { + std::lock_guard lock(mutex_); + s.value = value_; + } + return {s}; + } + + /** + * Increment the counter by 1. + * */ + void increment() + { + std::lock_guard lock(mutex_); + value_++; + } + + /** + * Increment the counter by the given value. + * */ + void increment(double value) + { + std::lock_guard lock(mutex_); + value_ += value; + } + + void reset() + { + std::lock_guard lock(mutex_); + value_ = 0; + } + + static std::string_view type() + { + return "counter"; + } + + private: + mutable std::mutex mutex_; + double value_{0}; +}; +} // namespace monitoring +} // namespace drogon diff --git a/lib/inc/drogon/utils/monitoring/Gauge.h b/lib/inc/drogon/utils/monitoring/Gauge.h new file mode 100644 index 0000000000..f060f9273b --- /dev/null +++ b/lib/inc/drogon/utils/monitoring/Gauge.h @@ -0,0 +1,109 @@ +/** + * + * Gauge.h + * An Tao + * + * Copyright 2018, An Tao. All rights reserved. + * https://github.com/an-tao/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#pragma once +#include +#include +#include + +namespace drogon +{ +namespace monitoring +{ +/** + * This class is used to collect samples for a gauge metric. + * */ +class Gauge : public Metric +{ + public: + /** + * Construct a gauge metric with a name and a help string. + * */ + Gauge(const std::string &name, + const std::vector &labelNames, + const std::vector &labelValues) noexcept(false) + : Metric(name, labelNames, labelValues) + { + } + + std::vector collect() const override + { + Sample s; + std::lock_guard lock(mutex_); + s.name = name_; + s.value = value_; + s.timestamp = timestamp_; + return {s}; + } + + /** + * Increment the counter by 1. + * */ + void increment() + { + std::lock_guard lock(mutex_); + value_ += 1; + } + + void decrement() + { + std::lock_guard lock(mutex_); + value_ -= 1; + } + + void decrement(double value) + { + std::lock_guard lock(mutex_); + value_ -= value; + } + + /** + * Increment the counter by the given value. + * */ + void increment(double value) + { + std::lock_guard lock(mutex_); + value_ += value; + } + + void reset() + { + std::lock_guard lock(mutex_); + value_ = 0; + } + + void set(double value) + { + std::lock_guard lock(mutex_); + value_ = value; + } + + static std::string_view type() + { + return "counter"; + } + + void setToCurrentTime() + { + std::lock_guard lock(mutex_); + timestamp_ = trantor::Date::now(); + } + + private: + mutable std::mutex mutex_; + double value_{0}; + trantor::Date timestamp_{0}; +}; +} // namespace monitoring +} // namespace drogon \ No newline at end of file diff --git a/lib/inc/drogon/utils/monitoring/Histogram.h b/lib/inc/drogon/utils/monitoring/Histogram.h new file mode 100644 index 0000000000..cea92d89fd --- /dev/null +++ b/lib/inc/drogon/utils/monitoring/Histogram.h @@ -0,0 +1,122 @@ +/** + * + * Histogram.h + * An Tao + * + * Copyright 2018, An Tao. All rights reserved. + * https://github.com/an-tao/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace drogon +{ +namespace monitoring +{ +/** + * This class is used to collect samples for a counter metric. + * */ +class DROGON_EXPORT Histogram : public Metric +{ + public: + struct TimeBucket + { + std::vector buckets; + uint64_t count{0}; + double sum{0}; + }; + + Histogram(const std::string &name, + const std::vector &labelNames, + const std::vector &labelValues, + const std::vector &bucketBoundaries, + const std::chrono::duration &maxAge, + uint64_t timeBucketsCount, + trantor::EventLoop *loop = nullptr) noexcept(false) + : Metric(name, labelNames, labelValues), + maxAge_(maxAge), + timeBucketCount_(timeBucketsCount), + bucketBoundaries_(bucketBoundaries) + { + if (loop == nullptr) + { + loopThreadPtr_ = std::make_unique(); + loopPtr_ = loopThreadPtr_->getLoop(); + loopThreadPtr_->run(); + } + else + { + loopPtr_ = loop; + } + if (maxAge > std::chrono::seconds(0)) + { + if (timeBucketsCount == 0) + { + throw std::runtime_error( + "timeBucketsCount must be greater than 0"); + } + } + timeBuckets_.emplace_back(); + // check the bucket boundaries are sorted + for (size_t i = 1; i < bucketBoundaries.size(); i++) + { + if (bucketBoundaries[i] <= bucketBoundaries[i - 1]) + { + throw std::runtime_error( + "The bucket boundaries must be sorted"); + } + } + } + + void observe(double value); + std::vector collect() const override; + + ~Histogram() override + { + if (timerId_ != trantor::InvalidTimerId) + { + loopPtr_->invalidateTimer(timerId_); + } + } + + static std::string_view type() + { + return "histogram"; + } + + private: + std::deque timeBuckets_; + std::unique_ptr loopThreadPtr_; + trantor::EventLoop *loopPtr_{nullptr}; + mutable std::mutex mutex_; + std::chrono::duration maxAge_; + trantor::TimerId timerId_{trantor::InvalidTimerId}; + size_t timeBucketCount_{0}; + const std::vector bucketBoundaries_; + + void rotateTimeBuckets() + { + std::lock_guard guard(mutex_); + TimeBucket bucket; + bucket.buckets.resize(bucketBoundaries_.size() + 1); + timeBuckets_.emplace_back(std::move(bucket)); + if (timeBuckets_.size() > timeBucketCount_) + { + auto expiredTimeBucket = timeBuckets_.front(); + timeBuckets_.erase(timeBuckets_.begin()); + } + } +}; +} // namespace monitoring +} // namespace drogon diff --git a/lib/inc/drogon/utils/monitoring/Metric.h b/lib/inc/drogon/utils/monitoring/Metric.h new file mode 100644 index 0000000000..92c16736e5 --- /dev/null +++ b/lib/inc/drogon/utils/monitoring/Metric.h @@ -0,0 +1,77 @@ +/** + * + * Metric.h + * An Tao + * + * Copyright 2018, An Tao. All rights reserved. + * https://github.com/an-tao/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace drogon +{ +namespace monitoring +{ +/** + * This class is used to collect samples for a metric. + * */ +class Metric : public std::enable_shared_from_this +{ + public: + /** + * Construct a metric with a name and a help string. + * */ + + Metric(const std::string &name, + const std::vector &labelNames, + const std::vector &labelValues) noexcept(false) + : name_(name) + { + if (labelNames.size() != labelValues.size()) + { + throw std::runtime_error( + "The number of label names is not equal to the number of label " + "values!"); + } + labels_.resize(labelNames.size()); + for (size_t i = 0; i < labelNames.size(); i++) + { + labels_[i].first = labelNames[i]; + labels_[i].second = labelValues[i]; + } + }; + + const std::string &name() const + { + return name_; + } + + const std::vector> &labels() const + { + return labels_; + } + + virtual ~Metric() = default; + virtual std::vector collect() const = 0; + + protected: + const std::string name_; + std::vector> labels_; +}; + +using MetricPtr = std::shared_ptr; + +} // namespace monitoring +} // namespace drogon \ No newline at end of file diff --git a/lib/inc/drogon/utils/monitoring/Registry.h b/lib/inc/drogon/utils/monitoring/Registry.h new file mode 100644 index 0000000000..fde6e5a0fc --- /dev/null +++ b/lib/inc/drogon/utils/monitoring/Registry.h @@ -0,0 +1,35 @@ +/** + * + * Registry.h + * An Tao + * + * Copyright 2018, An Tao. All rights reserved. + * https://github.com/an-tao/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#pragma once +#include + +namespace drogon +{ +namespace monitoring +{ +class CollectorBase; + +/** + * This class is used to register metrics. + * */ +class Registry +{ + public: + virtual ~Registry() = default; + virtual void registerCollector( + const std::shared_ptr &collector) = 0; +}; +} // namespace monitoring +} // namespace drogon \ No newline at end of file diff --git a/lib/inc/drogon/utils/monitoring/Sample.h b/lib/inc/drogon/utils/monitoring/Sample.h new file mode 100644 index 0000000000..b86e7c9221 --- /dev/null +++ b/lib/inc/drogon/utils/monitoring/Sample.h @@ -0,0 +1,35 @@ +/** + * + * Sample.h + * An Tao + * + * Copyright 2018, An Tao. All rights reserved. + * https://github.com/an-tao/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#pragma once +#include +#include +#include + +namespace drogon +{ +namespace monitoring +{ +/** + * This class is used to collect samples for a metric. + * */ +struct Sample +{ + double value{0}; + trantor::Date timestamp{0}; + std::string name; + std::vector> exLabels; +}; +} // namespace monitoring +} // namespace drogon \ No newline at end of file diff --git a/lib/inc/drogon/utils/monitoring/StopWatch.h b/lib/inc/drogon/utils/monitoring/StopWatch.h new file mode 100644 index 0000000000..9c4488a3a3 --- /dev/null +++ b/lib/inc/drogon/utils/monitoring/StopWatch.h @@ -0,0 +1,75 @@ +/** + * + * StopWatch.h + * An Tao + * + * Copyright 2018, An Tao. All rights reserved. + * https://github.com/an-tao/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#include +#include +#include + +namespace drogon +{ +/** + * @brief This class is used to measure the elapsed time. + */ +class StopWatch +{ + public: + StopWatch() : start_(std::chrono::steady_clock::now()) + { + } + + ~StopWatch() + { + } + + /** + * @brief Reset the start time. + */ + void reset() + { + start_ = std::chrono::steady_clock::now(); + } + + /** + * @brief Get the elapsed time in seconds. + */ + double elapsed() const + { + return std::chrono::duration_cast>( + std::chrono::steady_clock::now() - start_) + .count(); + } + + private: + std::chrono::steady_clock::time_point start_; +}; + +class LifeTimeWatch +{ + public: + LifeTimeWatch(std::function callback) + : stopWatch_(), callback_(std::move(callback)) + { + assert(callback_); + } + + ~LifeTimeWatch() + { + callback_(stopWatch_.elapsed()); + } + + private: + StopWatch stopWatch_; + std::function callback_; +}; +} // namespace drogon \ No newline at end of file diff --git a/lib/src/Histogram.cc b/lib/src/Histogram.cc new file mode 100644 index 0000000000..a62efdad84 --- /dev/null +++ b/lib/src/Histogram.cc @@ -0,0 +1,80 @@ +#include +using namespace drogon; +using namespace drogon::monitoring; + +void Histogram::observe(double value) +{ + std::lock_guard lock(mutex_); + if (maxAge_ > std::chrono::seconds(0) && + timerId_ == trantor::InvalidTimerId) + { + std::weak_ptr weakPtr = + std::dynamic_pointer_cast(shared_from_this()); + timerId_ = loopPtr_->runEvery(maxAge_ / timeBucketCount_, [weakPtr]() { + auto thisPtr = weakPtr.lock(); + if (!thisPtr) + return; + thisPtr->rotateTimeBuckets(); + }); + } + auto ¤tBucket = timeBuckets_.back(); + currentBucket.sum += value; + currentBucket.count += 1; + for (size_t i = 0; i < bucketBoundaries_.size(); i++) + { + if (value <= bucketBoundaries_[i]) + { + currentBucket.buckets[i] += 1; + break; + } + } + if (value > bucketBoundaries_.back()) + { + currentBucket.buckets.back() += 1; + } +} + +std::vector Histogram::collect() const +{ + std::vector samples; + std::lock_guard guard(mutex_); + size_t count{0}; + for (size_t i = 0; i < bucketBoundaries_.size(); i++) + { + Sample sample; + for (auto &bucket : timeBuckets_) + { + count += bucket.buckets[i]; + } + sample.name = name_ + "_bucket"; + sample.exLabels.emplace_back("le", + std::to_string(bucketBoundaries_[i])); + sample.value = count; + samples.emplace_back(std::move(sample)); + } + Sample sample; + for (auto &bucket : timeBuckets_) + { + count += bucket.buckets.back(); + } + sample.name = name_ + "_bucket"; + sample.exLabels.emplace_back("le", "+Inf"); + sample.value = count; + samples.emplace_back(std::move(sample)); + double sum{0}; + uint64_t totalCount{0}; + for (auto &bucket : timeBuckets_) + { + sum += bucket.sum; + totalCount += bucket.count; + } + Sample sumSample; + sumSample.name = name_ + "_sum"; + sumSample.value = sum; + samples.emplace_back(std::move(sumSample)); + Sample countSample; + countSample.name = name_ + "_count"; + countSample.value = totalCount; + samples.emplace_back(std::move(countSample)); + return samples; +} \ No newline at end of file diff --git a/lib/src/PromExporter.cc b/lib/src/PromExporter.cc new file mode 100644 index 0000000000..2371016ae7 --- /dev/null +++ b/lib/src/PromExporter.cc @@ -0,0 +1,207 @@ +#include +#include +#include +#include +#include +#include + +using namespace drogon; +using namespace drogon::monitoring; +using namespace drogon::plugin; + +void PromExporter::initAndStart(const Json::Value &config) +{ + path_ = config.get("path", path_).asString(); + LOG_ERROR << path_; + auto &app = drogon::app(); + std::weak_ptr weakPtr = shared_from_this(); + app.registerHandler( + path_, + [weakPtr](const HttpRequestPtr &req, + std::function &&callback) { + auto thisPtr = weakPtr.lock(); + if (!thisPtr) + { + auto resp = HttpResponse::newNotFoundResponse(); + callback(resp); + return; + } + auto resp = HttpResponse::newHttpResponse(); + resp->setBody(thisPtr->exportMetrics()); + resp->setExpiredTime(5); + callback(resp); + }, + {Get, Options}, + "PromExporter"); + if (config.isMember("collectors")) + { + std::lock_guard guard(mutex_); + auto &collectors = config["collectors"]; + if (collectors.isArray()) + { + for (auto const &collector : collectors) + { + if (collector.isObject()) + { + auto name = collector["name"].asString(); + auto type = collector["type"].asString(); + auto help = collector["help"].asString(); + auto labels = collector["labels"]; + if (labels.isArray()) + { + std::vector labelNames; + for (auto const &label : labels) + { + if (label.isString()) + { + labelNames.push_back(label.asString()); + } + else + { + LOG_ERROR << "label name must be a string!"; + } + } + if (type == "counter") + { + auto counterCollector = + std::make_shared>( + name, help, labelNames); + collectors_.insert( + std::make_pair(name, counterCollector)); + } + else if (type == "gauge") + { + auto gaugeCollector = + std::make_shared>(name, + help, + labelNames); + collectors_.insert( + std::make_pair(name, gaugeCollector)); + } + else if (type == "histogram") + { + auto histogramCollector = + std::make_shared>( + name, help, labelNames); + collectors_.insert( + std::make_pair(name, histogramCollector)); + } + else + { + LOG_ERROR << "Unknown collector type: " << type; + } + } + else + { + LOG_ERROR << "labels must be an array!"; + } + } + else + { + LOG_ERROR << "collector must be an object!"; + } + } + } + else + { + LOG_ERROR << "collectors must be an array!"; + } + } +} + +static std::string exportCollector( + const std::shared_ptr &collector) +{ + auto sampleGroups = collector->collect(); + std::string res; + res.append("# HELP ") + .append(collector->name()) + .append(" ") + .append(collector->help()) + .append("\r\n"); + res.append("# TYPE ") + .append(collector->name()) + .append(" ") + .append(collector->type()) + .append("\r\n"); + for (auto const &sampleGroup : sampleGroups) + { + auto const &metricPtr = sampleGroup.metric; + auto const &samples = sampleGroup.samples; + for (auto &sample : samples) + { + res.append(metricPtr->name()); + if (!sample.exLabels.empty() || !metricPtr->labels().empty()) + { + res.append("{"); + for (auto const &label : metricPtr->labels()) + { + res.append(label.first) + .append("=\"") + .append(label.second) + .append("\","); + } + for (auto const &label : sample.exLabels) + { + res.append(label.first) + .append("=\"") + .append(label.second) + .append("\","); + } + res.pop_back(); + res.append("}"); + } + res.append(" ").append(std::to_string(sample.value)); + if (sample.timestamp.microSecondsSinceEpoch() > 0) + { + res.append(" ") + .append(std::to_string( + sample.timestamp.microSecondsSinceEpoch() / 1000)) + .append("\r\n"); + } + else + { + res.append("\r\n"); + } + } + } + return res; +} + +std::string PromExporter::exportMetrics() +{ + std::lock_guard guard(mutex_); + std::string result; + for (auto const &collector : collectors_) + { + result.append(exportCollector(collector.second)); + } + return result; +} + +void PromExporter::registerCollector( + const std::shared_ptr &collector) +{ + std::lock_guard guard(mutex_); + if (collectors_.find(collector->name()) != collectors_.end()) + { + throw std::runtime_error("The collector named " + collector->name() + + " has been registered!"); + } + collectors_.insert(std::make_pair(collector->name(), collector)); +} + +std::shared_ptr PromExporter::getCollector( + const std::string &name) const noexcept(false) +{ + std::lock_guard guard(mutex_); + auto iter = collectors_.find(name); + if (iter != collectors_.end()) + { + return iter->second; + } + else + { + throw std::runtime_error("Can't find the collector named " + name); + } +} diff --git a/lib/tests/integration_test/server/TestController.cc b/lib/tests/integration_test/server/TestController.cc index 9f08736283..fbae7d44ee 100644 --- a/lib/tests/integration_test/server/TestController.cc +++ b/lib/tests/integration_test/server/TestController.cc @@ -6,6 +6,7 @@ void TestController::asyncHandleHttpRequest( std::function &&callback) { // write your application logic here + counter_->increment(); LOG_WARN << req->matchedPathPatternData(); LOG_DEBUG << "index=" << threadIndex_.getThreadData(); ++(threadIndex_.getThreadData()); diff --git a/lib/tests/integration_test/server/TestController.h b/lib/tests/integration_test/server/TestController.h index c2eaaffdae..3eb5e29144 100644 --- a/lib/tests/integration_test/server/TestController.h +++ b/lib/tests/integration_test/server/TestController.h @@ -1,6 +1,9 @@ #pragma once #include #include +#include +#include +#include using namespace drogon; namespace example @@ -23,9 +26,18 @@ class TestController : public drogon::HttpSimpleController TestController() { LOG_DEBUG << "TestController constructor"; + auto collector = std::make_shared< + drogon::monitoring::Collector>( + "test_counter", + "The counter for requests to the root url", + std::vector()); + counter_ = collector->metric(std::vector()); + collector->registerTo( + *app().getSharedPlugin()); } private: drogon::IOThreadStorage threadIndex_; + std::shared_ptr counter_; }; } // namespace example