mirror of
https://github.com/XRPLF/clio.git
synced 2025-12-06 17:27:58 +00:00
@@ -138,6 +138,10 @@ target_sources (clio PRIVATE
|
||||
## Util
|
||||
src/util/config/Config.cpp
|
||||
src/util/log/Logger.cpp
|
||||
src/util/prometheus/Http.cpp
|
||||
src/util/prometheus/Label.cpp
|
||||
src/util/prometheus/Metrics.cpp
|
||||
src/util/prometheus/Prometheus.cpp
|
||||
src/util/Random.cpp
|
||||
src/util/Taggable.cpp)
|
||||
|
||||
@@ -165,6 +169,11 @@ if (tests)
|
||||
unittests/SubscriptionManagerTests.cpp
|
||||
unittests/util/TestObject.cpp
|
||||
unittests/util/StringUtils.cpp
|
||||
unittests/util/prometheus/CounterTests.cpp
|
||||
unittests/util/prometheus/GaugeTests.cpp
|
||||
unittests/util/prometheus/HttpTests.cpp
|
||||
unittests/util/prometheus/LabelTests.cpp
|
||||
unittests/util/prometheus/MetricsTests.cpp
|
||||
# ETL
|
||||
unittests/etl/ExtractionDataPipeTests.cpp
|
||||
unittests/etl/ExtractorTests.cpp
|
||||
|
||||
@@ -251,8 +251,16 @@ For a better security `admin_password` could be provided in the `server` section
|
||||
}
|
||||
```
|
||||
If the password is presented in the config, clio will check the Authorization header (if any) in each request for the password.
|
||||
The Authorization header should contain type `Password` and the password from the config, e.g. `Password secret`.
|
||||
Exactly equal password gains admin rights for the request or a websocket connection.
|
||||
|
||||
## Prometheus metrics collection
|
||||
|
||||
Clio natively supports Prometheus metrics collection. It accepts Prometheus requests on the port configured in `server` section of config.
|
||||
Prometheus metrics are enabled by default. To disable it add `"prometheus_enabled": false` to the config.
|
||||
It is important to know that clio responds to Prometheus request only if they are admin requests, so Prometheus should be configured to send admin password in header.
|
||||
There is an example of docker-compose file, Prometheus and Grafana configs in [examples/infrastructure](examples/infrastructure).
|
||||
|
||||
## Using clang-tidy for static analysis
|
||||
|
||||
Minimum clang-tidy version required is 16.0.
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"log_level": "trace"
|
||||
}
|
||||
],
|
||||
"prometheus_enabled": true,
|
||||
"log_level": "info",
|
||||
// Log format (this is the default format)
|
||||
"log_format": "%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%",
|
||||
|
||||
25
examples/infrastructure/README.md
Normal file
25
examples/infrastructure/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Example of clio monitoring infrastructure
|
||||
|
||||
This directory contains an example of docker based infrastructure to collect and visualise metrics from clio.
|
||||
|
||||
The structure of the directory:
|
||||
- `compose.yaml`
|
||||
Docker-compose file with Prometheus and Grafana set up.
|
||||
- `prometheus.yaml`
|
||||
Defines metrics collection from Clio and Prometheus itself.
|
||||
Demonstrates how to setup Clio target and Clio's admin authorisation in Prometheus.
|
||||
- `grafana/clio_dashboard.json`
|
||||
Json file containing preconfigured dashboard in Grafana format.
|
||||
- `grafana/dashboard_local.yaml`
|
||||
Grafana configuration file defining the directory to search for dashboards json files.
|
||||
- `grafana/datasources.yaml`
|
||||
Grafana configuration file defining Prometheus as a data source for Grafana.
|
||||
|
||||
## How to try
|
||||
|
||||
1. Make sure you have `docker` and `docker-compose` installed.
|
||||
2. Run `docker-compose up -d` from this directory. It will start docker containers with Prometheus and Grafana.
|
||||
3. Open [http://localhost:3000/dashboards](http://localhost:3000/dashboards). Grafana login `admin`, password `grafana`.
|
||||
There will be preconfigured Clio dashboard.
|
||||
|
||||
If Clio is not running yet launch Clio to see metrics. Some of the metrics may appear only after requests to Clio.
|
||||
20
examples/infrastructure/compose.yaml
Normal file
20
examples/infrastructure/compose.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
volumes:
|
||||
- ./prometheus.yaml:/etc/prometheus/prometheus.yml
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
ports:
|
||||
- 3000:3000
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
- GF_SECURITY_ADMIN_PASSWORD=grafana
|
||||
volumes:
|
||||
- ./grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
|
||||
- ./grafana/dashboard_local.yaml:/etc/grafana/provisioning/dashboards/local.yaml
|
||||
- ./grafana/clio_dashboard.json:/var/lib/grafana/dashboards/clio_dashboard.json
|
||||
1102
examples/infrastructure/grafana/clio_dashboard.json
Normal file
1102
examples/infrastructure/grafana/clio_dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
23
examples/infrastructure/grafana/dashboard_local.yaml
Normal file
23
examples/infrastructure/grafana/dashboard_local.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'Clio dashboard'
|
||||
# <int> Org id. Default to 1
|
||||
orgId: 1
|
||||
# <string> name of the dashboard folder.
|
||||
folder: ''
|
||||
# <string> folder UID. will be automatically generated if not specified
|
||||
folderUid: ''
|
||||
# <string> provider type. Default to 'file'
|
||||
type: file
|
||||
# <bool> disable dashboard deletion
|
||||
disableDeletion: false
|
||||
# <int> how often Grafana will scan for changed dashboards
|
||||
updateIntervalSeconds: 10
|
||||
# <bool> allow updating provisioned dashboards from the UI
|
||||
allowUiUpdates: false
|
||||
options:
|
||||
# <string, required> path to dashboard files on disk. Required when using the 'file' type
|
||||
path: /var/lib/grafana/dashboards
|
||||
# <bool> use folder names from filesystem to create folders in Grafana
|
||||
foldersFromFilesStructure: true
|
||||
8
examples/infrastructure/grafana/datasources.yaml
Normal file
8
examples/infrastructure/grafana/datasources.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
access: proxy
|
||||
19
examples/infrastructure/prometheus.yaml
Normal file
19
examples/infrastructure/prometheus.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
scrape_configs:
|
||||
- job_name: clio
|
||||
scrape_interval: 5s
|
||||
scrape_timeout: 5s
|
||||
authorization:
|
||||
type: Password
|
||||
# sha256sum from password `xrp`
|
||||
# use echo -n 'your_password' | shasum -a 256 to get hash
|
||||
credentials: 0e1dcf1ff020cceabf8f4a60a32e814b5b46ee0bb8cd4af5c814e4071bd86a18
|
||||
static_configs:
|
||||
- targets:
|
||||
- host.docker.internal:51233
|
||||
- job_name: prometheus
|
||||
honor_timestamps: true
|
||||
scrape_interval: 15s
|
||||
scrape_timeout: 10s
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:9090
|
||||
@@ -19,8 +19,33 @@
|
||||
|
||||
#include <data/BackendCounters.h>
|
||||
|
||||
#include <util/prometheus/Prometheus.h>
|
||||
|
||||
namespace data {
|
||||
|
||||
using namespace util::prometheus;
|
||||
|
||||
BackendCounters::BackendCounters()
|
||||
: tooBusyCounter_(PrometheusService::counterInt(
|
||||
"backend_too_busy_total_number",
|
||||
Labels(),
|
||||
"The total number of times the backend was too busy to process a request"
|
||||
))
|
||||
, writeSyncCounter_(PrometheusService::counterInt(
|
||||
"backend_operations_total_number",
|
||||
Labels({Label{"operation", "write_sync"}}),
|
||||
"The total number of times the backend had to write synchronously"
|
||||
))
|
||||
, writeSyncRetryCounter_(PrometheusService::counterInt(
|
||||
"backend_operations_total_number",
|
||||
Labels({Label{"operation", "write_sync_retry"}}),
|
||||
"The total number of times the backend had to retry a synchronous write"
|
||||
))
|
||||
, asyncWriteCounters_{"write_async"}
|
||||
, asyncReadCounters_{"read_async"}
|
||||
{
|
||||
}
|
||||
|
||||
BackendCounters::PtrType
|
||||
BackendCounters::make()
|
||||
{
|
||||
@@ -31,19 +56,19 @@ BackendCounters::make()
|
||||
void
|
||||
BackendCounters::registerTooBusy()
|
||||
{
|
||||
++tooBusyCounter_;
|
||||
++tooBusyCounter_.get();
|
||||
}
|
||||
|
||||
void
|
||||
BackendCounters::registerWriteSync()
|
||||
{
|
||||
++writeSyncCounter_;
|
||||
++writeSyncCounter_.get();
|
||||
}
|
||||
|
||||
void
|
||||
BackendCounters::registerWriteSyncRetry()
|
||||
{
|
||||
++writeSyncRetryCounter_;
|
||||
++writeSyncRetryCounter_.get();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -92,9 +117,9 @@ boost::json::object
|
||||
BackendCounters::report() const
|
||||
{
|
||||
boost::json::object result;
|
||||
result["too_busy"] = tooBusyCounter_;
|
||||
result["write_sync"] = writeSyncCounter_;
|
||||
result["write_sync_retry"] = writeSyncRetryCounter_;
|
||||
result["too_busy"] = tooBusyCounter_.get().value();
|
||||
result["write_sync"] = writeSyncCounter_.get().value();
|
||||
result["write_sync_retry"] = writeSyncRetryCounter_.get().value();
|
||||
for (auto const& [key, value] : asyncWriteCounters_.report())
|
||||
result[key] = value;
|
||||
for (auto const& [key, value] : asyncReadCounters_.report())
|
||||
@@ -102,46 +127,67 @@ BackendCounters::report() const
|
||||
return result;
|
||||
}
|
||||
|
||||
BackendCounters::AsyncOperationCounters::AsyncOperationCounters(std::string name) : name_(std::move(name))
|
||||
BackendCounters::AsyncOperationCounters::AsyncOperationCounters(std::string name)
|
||||
: name_(std::move(name))
|
||||
, pendingCounter_(PrometheusService::gaugeInt(
|
||||
"backend_operations_current_number",
|
||||
Labels({{"operation", name_}, {"status", "pending"}}),
|
||||
"The current number of pending " + name_ + " operations"
|
||||
))
|
||||
, completedCounter_(PrometheusService::counterInt(
|
||||
"backend_operations_total_number",
|
||||
Labels({{"operation", name_}, {"status", "completed"}}),
|
||||
"The total number of completed " + name_ + " operations"
|
||||
))
|
||||
, retryCounter_(PrometheusService::counterInt(
|
||||
"backend_operations_total_number",
|
||||
Labels({{"operation", name_}, {"status", "retry"}}),
|
||||
"The total number of retried " + name_ + " operations"
|
||||
))
|
||||
, errorCounter_(PrometheusService::counterInt(
|
||||
"backend_operations_total_number",
|
||||
Labels({{"operation", name_}, {"status", "error"}}),
|
||||
"The total number of errored " + name_ + " operations"
|
||||
))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
BackendCounters::AsyncOperationCounters::registerStarted(std::uint64_t const count)
|
||||
{
|
||||
pendingCounter_ += count;
|
||||
pendingCounter_.get() += count;
|
||||
}
|
||||
|
||||
void
|
||||
BackendCounters::AsyncOperationCounters::registerFinished(std::uint64_t const count)
|
||||
{
|
||||
assert(pendingCounter_ >= count);
|
||||
pendingCounter_ -= count;
|
||||
completedCounter_ += count;
|
||||
assert(pendingCounter_.value() >= count);
|
||||
pendingCounter_.get() -= count;
|
||||
completedCounter_.get() += count;
|
||||
}
|
||||
|
||||
void
|
||||
BackendCounters::AsyncOperationCounters::registerRetry(std::uint64_t count)
|
||||
{
|
||||
retryCounter_ += count;
|
||||
retryCounter_.get() += count;
|
||||
}
|
||||
|
||||
void
|
||||
BackendCounters::AsyncOperationCounters::registerError(std::uint64_t count)
|
||||
{
|
||||
assert(pendingCounter_ >= count);
|
||||
pendingCounter_ -= count;
|
||||
errorCounter_ += count;
|
||||
assert(pendingCounter_.value() >= count);
|
||||
pendingCounter_.get() -= count;
|
||||
errorCounter_.get() += count;
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
BackendCounters::AsyncOperationCounters::report() const
|
||||
{
|
||||
return boost::json::object{
|
||||
{name_ + "_pending", pendingCounter_},
|
||||
{name_ + "_completed", completedCounter_},
|
||||
{name_ + "_retry", retryCounter_},
|
||||
{name_ + "_error", errorCounter_}};
|
||||
{name_ + "_pending", pendingCounter_.get().value()},
|
||||
{name_ + "_completed", completedCounter_.get().value()},
|
||||
{name_ + "_retry", retryCounter_.get().value()},
|
||||
{name_ + "_error", errorCounter_.get().value()}};
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <util/prometheus/Prometheus.h>
|
||||
|
||||
#include <boost/json/object.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
@@ -50,6 +53,7 @@ concept SomeBackendCounters = requires(T a) {
|
||||
|
||||
/**
|
||||
* @brief Holds statistics about the backend.
|
||||
*
|
||||
* @note This class is thread-safe.
|
||||
*/
|
||||
class BackendCounters {
|
||||
@@ -93,7 +97,7 @@ public:
|
||||
report() const;
|
||||
|
||||
private:
|
||||
BackendCounters() = default;
|
||||
BackendCounters();
|
||||
|
||||
class AsyncOperationCounters {
|
||||
public:
|
||||
@@ -116,16 +120,16 @@ private:
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::atomic_uint64_t pendingCounter_ = 0u;
|
||||
std::atomic_uint64_t completedCounter_ = 0u;
|
||||
std::atomic_uint64_t retryCounter_ = 0u;
|
||||
std::atomic_uint64_t errorCounter_ = 0u;
|
||||
std::reference_wrapper<util::prometheus::GaugeInt> pendingCounter_;
|
||||
std::reference_wrapper<util::prometheus::CounterInt> completedCounter_;
|
||||
std::reference_wrapper<util::prometheus::CounterInt> retryCounter_;
|
||||
std::reference_wrapper<util::prometheus::CounterInt> errorCounter_;
|
||||
};
|
||||
|
||||
std::atomic_uint64_t tooBusyCounter_ = 0u;
|
||||
std::reference_wrapper<util::prometheus::CounterInt> tooBusyCounter_;
|
||||
|
||||
std::atomic_uint64_t writeSyncCounter_ = 0u;
|
||||
std::atomic_uint64_t writeSyncRetryCounter_ = 0u;
|
||||
std::reference_wrapper<util::prometheus::CounterInt> writeSyncCounter_;
|
||||
std::reference_wrapper<util::prometheus::CounterInt> writeSyncRetryCounter_;
|
||||
|
||||
AsyncOperationCounters asyncWriteCounters_{"write_async"};
|
||||
AsyncOperationCounters asyncReadCounters_{"read_async"};
|
||||
|
||||
@@ -64,13 +64,13 @@ LedgerCache::getSuccessor(ripple::uint256 const& key, uint32_t seq) const
|
||||
if (!full_)
|
||||
return {};
|
||||
std::shared_lock const lck{mtx_};
|
||||
successorReqCounter_++;
|
||||
++successorReqCounter_.get();
|
||||
if (seq != latestSeq_)
|
||||
return {};
|
||||
auto e = map_.upper_bound(key);
|
||||
if (e == map_.end())
|
||||
return {};
|
||||
successorHitCounter_++;
|
||||
++successorHitCounter_.get();
|
||||
return {{e->first, e->second.blob}};
|
||||
}
|
||||
|
||||
@@ -95,13 +95,13 @@ LedgerCache::get(ripple::uint256 const& key, uint32_t seq) const
|
||||
std::shared_lock const lck{mtx_};
|
||||
if (seq > latestSeq_)
|
||||
return {};
|
||||
objectReqCounter_++;
|
||||
++objectReqCounter_.get();
|
||||
auto e = map_.find(key);
|
||||
if (e == map_.end())
|
||||
return {};
|
||||
if (seq < e->second.seq)
|
||||
return {};
|
||||
objectHitCounter_++;
|
||||
++objectHitCounter_.get();
|
||||
return {e->second.blob};
|
||||
}
|
||||
|
||||
@@ -138,17 +138,17 @@ LedgerCache::size() const
|
||||
float
|
||||
LedgerCache::getObjectHitRate() const
|
||||
{
|
||||
if (objectReqCounter_ == 0u)
|
||||
if (objectReqCounter_.get().value() == 0u)
|
||||
return 1;
|
||||
return static_cast<float>(objectHitCounter_) / objectReqCounter_;
|
||||
return static_cast<float>(objectHitCounter_.get().value()) / objectReqCounter_.get().value();
|
||||
}
|
||||
|
||||
float
|
||||
LedgerCache::getSuccessorHitRate() const
|
||||
{
|
||||
if (successorReqCounter_ == 0u)
|
||||
if (successorReqCounter_.get().value() == 0u)
|
||||
return 1;
|
||||
return static_cast<float>(successorHitCounter_) / successorReqCounter_;
|
||||
return static_cast<float>(successorHitCounter_.get().value()) / successorReqCounter_.get().value();
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <util/prometheus/Prometheus.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -40,12 +41,26 @@ class LedgerCache {
|
||||
};
|
||||
|
||||
// counters for fetchLedgerObject(s) hit rate
|
||||
mutable std::atomic_uint32_t objectReqCounter_ = 0;
|
||||
mutable std::atomic_uint32_t objectHitCounter_ = 0;
|
||||
std::reference_wrapper<util::prometheus::CounterInt> objectReqCounter_{PrometheusService::counterInt(
|
||||
"ledger_cache_counter_total_number",
|
||||
util::prometheus::Labels({{"type", "request"}, {"fetch", "ledger_objects"}}),
|
||||
"LedgerCache statistics"
|
||||
)};
|
||||
std::reference_wrapper<util::prometheus::CounterInt> objectHitCounter_{PrometheusService::counterInt(
|
||||
"ledger_cache_counter_total_number",
|
||||
util::prometheus::Labels({{"type", "cache_hit"}, {"fetch", "ledger_objects"}})
|
||||
)};
|
||||
|
||||
// counters for fetchSuccessorKey hit rate
|
||||
mutable std::atomic_uint32_t successorReqCounter_ = 0;
|
||||
mutable std::atomic_uint32_t successorHitCounter_ = 0;
|
||||
std::reference_wrapper<util::prometheus::CounterInt> successorReqCounter_{PrometheusService::counterInt(
|
||||
"ledger_cache_counter_total_number",
|
||||
util::prometheus::Labels({{"type", "request"}, {"fetch", "successor_key"}}),
|
||||
"ledgerCache"
|
||||
)};
|
||||
std::reference_wrapper<util::prometheus::CounterInt> successorHitCounter_{PrometheusService::counterInt(
|
||||
"ledger_cache_counter_total_number",
|
||||
util::prometheus::Labels({{"type", "cache_hit"}, {"fetch", "successor_key"}})
|
||||
)};
|
||||
|
||||
std::map<ripple::uint256, CacheEntry> map_;
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <data/BackendInterface.h>
|
||||
#include <util/config/Config.h>
|
||||
#include <util/log/Logger.h>
|
||||
#include <util/prometheus/Prometheus.h>
|
||||
#include <web/interface/ConnectionBase.h>
|
||||
|
||||
#include <ripple/protocol/LedgerHeader.h>
|
||||
@@ -44,7 +45,7 @@ using SessionPtrType = std::shared_ptr<web::ConnectionBase>;
|
||||
*/
|
||||
template <class T>
|
||||
inline void
|
||||
sendToSubscribers(std::shared_ptr<std::string> const& message, T& subscribers, std::atomic_uint64_t& counter)
|
||||
sendToSubscribers(std::shared_ptr<std::string> const& message, T& subscribers, util::prometheus::GaugeInt& counter)
|
||||
{
|
||||
for (auto it = subscribers.begin(); it != subscribers.end();) {
|
||||
auto& session = *it;
|
||||
@@ -67,7 +68,7 @@ sendToSubscribers(std::shared_ptr<std::string> const& message, T& subscribers, s
|
||||
*/
|
||||
template <class T>
|
||||
inline void
|
||||
addSession(SessionPtrType session, T& subscribers, std::atomic_uint64_t& counter)
|
||||
addSession(SessionPtrType session, T& subscribers, util::prometheus::GaugeInt& counter)
|
||||
{
|
||||
if (!subscribers.contains(session)) {
|
||||
subscribers.insert(session);
|
||||
@@ -84,7 +85,7 @@ addSession(SessionPtrType session, T& subscribers, std::atomic_uint64_t& counter
|
||||
*/
|
||||
template <class T>
|
||||
inline void
|
||||
removeSession(SessionPtrType session, T& subscribers, std::atomic_uint64_t& counter)
|
||||
removeSession(SessionPtrType session, T& subscribers, util::prometheus::GaugeInt& counter)
|
||||
{
|
||||
if (subscribers.contains(session)) {
|
||||
subscribers.erase(session);
|
||||
@@ -98,7 +99,7 @@ removeSession(SessionPtrType session, T& subscribers, std::atomic_uint64_t& coun
|
||||
class Subscription {
|
||||
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
|
||||
std::unordered_set<SessionPtrType> subscribers_ = {};
|
||||
std::atomic_uint64_t subCount_ = 0;
|
||||
util::prometheus::GaugeInt& subCount_;
|
||||
|
||||
public:
|
||||
Subscription() = delete;
|
||||
@@ -110,7 +111,13 @@ public:
|
||||
*
|
||||
* @param ioc The io_context to run on
|
||||
*/
|
||||
explicit Subscription(boost::asio::io_context& ioc) : strand_(boost::asio::make_strand(ioc))
|
||||
explicit Subscription(boost::asio::io_context& ioc, std::string const& name)
|
||||
: strand_(boost::asio::make_strand(ioc))
|
||||
, subCount_(PrometheusService::gaugeInt(
|
||||
"subscriptions_current_number",
|
||||
util::prometheus::Labels({util::prometheus::Label{"stream", name}}),
|
||||
fmt::format("Current subscribers number on the {} stream", name)
|
||||
))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -155,7 +162,7 @@ public:
|
||||
std::uint64_t
|
||||
count() const
|
||||
{
|
||||
return subCount_.load();
|
||||
return subCount_.value();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,7 +184,7 @@ class SubscriptionMap {
|
||||
|
||||
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
|
||||
std::unordered_map<Key, SubscribersType> subscribers_ = {};
|
||||
std::atomic_uint64_t subCount_ = 0;
|
||||
util::prometheus::GaugeInt& subCount_;
|
||||
|
||||
public:
|
||||
SubscriptionMap() = delete;
|
||||
@@ -189,7 +196,13 @@ public:
|
||||
*
|
||||
* @param ioc The io_context to run on
|
||||
*/
|
||||
explicit SubscriptionMap(boost::asio::io_context& ioc) : strand_(boost::asio::make_strand(ioc))
|
||||
explicit SubscriptionMap(boost::asio::io_context& ioc, std::string const& name)
|
||||
: strand_(boost::asio::make_strand(ioc))
|
||||
, subCount_(PrometheusService::gaugeInt(
|
||||
"subscriptions_current_number",
|
||||
util::prometheus::Labels({util::prometheus::Label{"collection", name}}),
|
||||
fmt::format("Current subscribers number on the {} collection", name)
|
||||
))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -271,7 +284,7 @@ public:
|
||||
std::uint64_t
|
||||
count() const
|
||||
{
|
||||
return subCount_.load();
|
||||
return subCount_.value();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -319,15 +332,15 @@ public:
|
||||
* @param backend The backend to use
|
||||
*/
|
||||
SubscriptionManager(std::uint64_t numThreads, std::shared_ptr<data::BackendInterface const> const& backend)
|
||||
: ledgerSubscribers_(ioc_)
|
||||
, txSubscribers_(ioc_)
|
||||
, txProposedSubscribers_(ioc_)
|
||||
, manifestSubscribers_(ioc_)
|
||||
, validationsSubscribers_(ioc_)
|
||||
, bookChangesSubscribers_(ioc_)
|
||||
, accountSubscribers_(ioc_)
|
||||
, accountProposedSubscribers_(ioc_)
|
||||
, bookSubscribers_(ioc_)
|
||||
: ledgerSubscribers_(ioc_, "ledger")
|
||||
, txSubscribers_(ioc_, "tx")
|
||||
, txProposedSubscribers_(ioc_, "tx_proposed")
|
||||
, manifestSubscribers_(ioc_, "manifest")
|
||||
, validationsSubscribers_(ioc_, "validations")
|
||||
, bookChangesSubscribers_(ioc_, "book_changes")
|
||||
, accountSubscribers_(ioc_, "account")
|
||||
, accountProposedSubscribers_(ioc_, "account_proposed")
|
||||
, bookSubscribers_(ioc_, "book")
|
||||
, backend_(backend)
|
||||
{
|
||||
work_.emplace(ioc_);
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <rpc/RPCEngine.h>
|
||||
#include <rpc/common/impl/HandlerProvider.h>
|
||||
#include <util/config/Config.h>
|
||||
#include <util/prometheus/Prometheus.h>
|
||||
#include <web/RPCServerHandler.h>
|
||||
#include <web/Server.h>
|
||||
|
||||
@@ -156,6 +157,8 @@ try {
|
||||
LogService::init(config);
|
||||
LOG(LogService::info()) << "Clio version: " << Build::getClioFullVersionString();
|
||||
|
||||
PrometheusService::init(config);
|
||||
|
||||
auto const threads = config.valueOr("io_threads", 2);
|
||||
if (threads <= 0) {
|
||||
LOG(LogService::fatal()) << "io_threads is less than 1";
|
||||
|
||||
@@ -23,78 +23,161 @@
|
||||
|
||||
namespace rpc {
|
||||
|
||||
using util::prometheus::Label;
|
||||
using util::prometheus::Labels;
|
||||
|
||||
Counters::MethodInfo::MethodInfo(std::string const& method)
|
||||
: started(PrometheusService::counterInt(
|
||||
"rpc_method_total_number",
|
||||
Labels{{{"status", "started"}, {"method", method}}},
|
||||
fmt::format("Total number of started calls to the method {}", method)
|
||||
))
|
||||
, finished(PrometheusService::counterInt(
|
||||
"rpc_method_total_number",
|
||||
Labels{{{"status", "finished"}, {"method", method}}},
|
||||
fmt::format("Total number of finished calls to the method {}", method)
|
||||
))
|
||||
, failed(PrometheusService::counterInt(
|
||||
"rpc_method_total_number",
|
||||
Labels{{{"status", "failed"}, {"method", method}}},
|
||||
fmt::format("Total number of failed calls to the method {}", method)
|
||||
))
|
||||
, errored(PrometheusService::counterInt(
|
||||
"rpc_method_total_number",
|
||||
Labels{{{"status", "errored"}, {"method", method}}},
|
||||
fmt::format("Total number of errored calls to the method {}", method)
|
||||
))
|
||||
, forwarded(PrometheusService::counterInt(
|
||||
"rpc_method_total_number",
|
||||
Labels{{{"status", "forwarded"}, {"method", method}}},
|
||||
fmt::format("Total number of forwarded calls to the method {}", method)
|
||||
))
|
||||
, failedForward(PrometheusService::counterInt(
|
||||
"rpc_method_total_number",
|
||||
Labels{{{"status", "failed_forward"}, {"method", method}}},
|
||||
fmt::format("Total number of failed forwarded calls to the method {}", method)
|
||||
))
|
||||
, duration(PrometheusService::counterInt(
|
||||
"rpc_method_duration_us",
|
||||
Labels({util::prometheus::Label{"method", method}}),
|
||||
fmt::format("Total duration of calls to the method {}", method)
|
||||
))
|
||||
{
|
||||
}
|
||||
|
||||
Counters::MethodInfo&
|
||||
Counters::getMethodInfo(std::string const& method)
|
||||
{
|
||||
auto it = methodInfo_.find(method);
|
||||
if (it == methodInfo_.end()) {
|
||||
it = methodInfo_.emplace(method, MethodInfo(method)).first;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
Counters::Counters(WorkQueue const& wq)
|
||||
: tooBusyCounter_(PrometheusService::counterInt(
|
||||
"rpc_error_total_number",
|
||||
Labels({Label{"error_type", "too_busy"}}),
|
||||
"Total number of too busy errors"
|
||||
))
|
||||
, notReadyCounter_(PrometheusService::counterInt(
|
||||
"rpc_error_total_number",
|
||||
Labels({Label{"error_type", "not_ready"}}),
|
||||
"Total number of not ready replyes"
|
||||
))
|
||||
, badSyntaxCounter_(PrometheusService::counterInt(
|
||||
"rpc_error_total_number",
|
||||
Labels({Label{"error_type", "bad_syntax"}}),
|
||||
"Total number of bad syntax replyes"
|
||||
))
|
||||
, unknownCommandCounter_(PrometheusService::counterInt(
|
||||
"rpc_error_total_number",
|
||||
Labels({Label{"error_type", "unknown_command"}}),
|
||||
"Total number of unknown command replyes"
|
||||
))
|
||||
, internalErrorCounter_(PrometheusService::counterInt(
|
||||
"rpc_error_total_number",
|
||||
Labels({Label{"error_type", "internal_error"}}),
|
||||
"Total number of internal errors"
|
||||
))
|
||||
, workQueue_(std::cref(wq))
|
||||
, startupTime_{std::chrono::system_clock::now()}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
Counters::rpcFailed(std::string const& method)
|
||||
{
|
||||
std::scoped_lock const lk(mutex_);
|
||||
MethodInfo& counters = methodInfo_[method];
|
||||
++counters.started;
|
||||
++counters.failed;
|
||||
MethodInfo const& counters = getMethodInfo(method);
|
||||
++counters.started.get();
|
||||
++counters.failed.get();
|
||||
}
|
||||
|
||||
void
|
||||
Counters::rpcErrored(std::string const& method)
|
||||
{
|
||||
std::scoped_lock const lk(mutex_);
|
||||
MethodInfo& counters = methodInfo_[method];
|
||||
++counters.started;
|
||||
++counters.errored;
|
||||
MethodInfo const& counters = getMethodInfo(method);
|
||||
++counters.started.get();
|
||||
++counters.errored.get();
|
||||
}
|
||||
|
||||
void
|
||||
Counters::rpcComplete(std::string const& method, std::chrono::microseconds const& rpcDuration)
|
||||
{
|
||||
std::scoped_lock const lk(mutex_);
|
||||
MethodInfo& counters = methodInfo_[method];
|
||||
++counters.started;
|
||||
++counters.finished;
|
||||
counters.duration += rpcDuration.count();
|
||||
MethodInfo const& counters = getMethodInfo(method);
|
||||
++counters.started.get();
|
||||
++counters.finished.get();
|
||||
counters.duration.get() += rpcDuration.count();
|
||||
}
|
||||
|
||||
void
|
||||
Counters::rpcForwarded(std::string const& method)
|
||||
{
|
||||
std::scoped_lock const lk(mutex_);
|
||||
MethodInfo& counters = methodInfo_[method];
|
||||
++counters.forwarded;
|
||||
MethodInfo const& counters = getMethodInfo(method);
|
||||
++counters.forwarded.get();
|
||||
}
|
||||
|
||||
void
|
||||
Counters::rpcFailedToForward(std::string const& method)
|
||||
{
|
||||
std::scoped_lock const lk(mutex_);
|
||||
MethodInfo& counters = methodInfo_[method];
|
||||
++counters.failedForward;
|
||||
MethodInfo const& counters = getMethodInfo(method);
|
||||
++counters.failedForward.get();
|
||||
}
|
||||
|
||||
void
|
||||
Counters::onTooBusy()
|
||||
{
|
||||
++tooBusyCounter_;
|
||||
++tooBusyCounter_.get();
|
||||
}
|
||||
|
||||
void
|
||||
Counters::onNotReady()
|
||||
{
|
||||
++notReadyCounter_;
|
||||
++notReadyCounter_.get();
|
||||
}
|
||||
|
||||
void
|
||||
Counters::onBadSyntax()
|
||||
{
|
||||
++badSyntaxCounter_;
|
||||
++badSyntaxCounter_.get();
|
||||
}
|
||||
|
||||
void
|
||||
Counters::onUnknownCommand()
|
||||
{
|
||||
++unknownCommandCounter_;
|
||||
++unknownCommandCounter_.get();
|
||||
}
|
||||
|
||||
void
|
||||
Counters::onInternalError()
|
||||
{
|
||||
++internalErrorCounter_;
|
||||
++internalErrorCounter_.get();
|
||||
}
|
||||
|
||||
std::chrono::seconds
|
||||
@@ -114,22 +197,22 @@ Counters::report() const
|
||||
|
||||
for (auto const& [method, info] : methodInfo_) {
|
||||
auto counters = boost::json::object{};
|
||||
counters[JS(started)] = std::to_string(info.started);
|
||||
counters[JS(finished)] = std::to_string(info.finished);
|
||||
counters[JS(errored)] = std::to_string(info.errored);
|
||||
counters[JS(failed)] = std::to_string(info.failed);
|
||||
counters["forwarded"] = std::to_string(info.forwarded);
|
||||
counters["failed_forward"] = std::to_string(info.failedForward);
|
||||
counters[JS(duration_us)] = std::to_string(info.duration);
|
||||
counters[JS(started)] = std::to_string(info.started.get().value());
|
||||
counters[JS(finished)] = std::to_string(info.finished.get().value());
|
||||
counters[JS(errored)] = std::to_string(info.errored.get().value());
|
||||
counters[JS(failed)] = std::to_string(info.failed.get().value());
|
||||
counters["forwarded"] = std::to_string(info.forwarded.get().value());
|
||||
counters["failed_forward"] = std::to_string(info.failedForward.get().value());
|
||||
counters[JS(duration_us)] = std::to_string(info.duration.get().value());
|
||||
|
||||
rpc[method] = std::move(counters);
|
||||
}
|
||||
|
||||
obj["too_busy_errors"] = std::to_string(tooBusyCounter_);
|
||||
obj["not_ready_errors"] = std::to_string(notReadyCounter_);
|
||||
obj["bad_syntax_errors"] = std::to_string(badSyntaxCounter_);
|
||||
obj["unknown_command_errors"] = std::to_string(unknownCommandCounter_);
|
||||
obj["internal_errors"] = std::to_string(internalErrorCounter_);
|
||||
obj["too_busy_errors"] = std::to_string(tooBusyCounter_.get().value());
|
||||
obj["not_ready_errors"] = std::to_string(notReadyCounter_.get().value());
|
||||
obj["bad_syntax_errors"] = std::to_string(badSyntaxCounter_.get().value());
|
||||
obj["unknown_command_errors"] = std::to_string(unknownCommandCounter_.get().value());
|
||||
obj["internal_errors"] = std::to_string(internalErrorCounter_.get().value());
|
||||
|
||||
obj["work_queue"] = workQueue_.get().report();
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <rpc/WorkQueue.h>
|
||||
#include <util/prometheus/Prometheus.h>
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
@@ -34,28 +35,34 @@ namespace rpc {
|
||||
* @brief Holds information about successful, failed, forwarded, etc. RPC handler calls.
|
||||
*/
|
||||
class Counters {
|
||||
using CounterType = std::reference_wrapper<util::prometheus::CounterInt>;
|
||||
/**
|
||||
* @brief All counters the system keeps track of for each RPC method.
|
||||
*/
|
||||
struct MethodInfo {
|
||||
std::uint64_t started = 0u;
|
||||
std::uint64_t finished = 0u;
|
||||
std::uint64_t failed = 0u;
|
||||
std::uint64_t errored = 0u;
|
||||
std::uint64_t forwarded = 0u;
|
||||
std::uint64_t failedForward = 0u;
|
||||
std::uint64_t duration = 0u;
|
||||
MethodInfo(std::string const& method);
|
||||
|
||||
CounterType started;
|
||||
CounterType finished;
|
||||
CounterType failed;
|
||||
CounterType errored;
|
||||
CounterType forwarded;
|
||||
CounterType failedForward;
|
||||
CounterType duration;
|
||||
};
|
||||
|
||||
MethodInfo&
|
||||
getMethodInfo(std::string const& method);
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::unordered_map<std::string, MethodInfo> methodInfo_;
|
||||
|
||||
// counters that don't carry RPC method information
|
||||
std::atomic_uint64_t tooBusyCounter_;
|
||||
std::atomic_uint64_t notReadyCounter_;
|
||||
std::atomic_uint64_t badSyntaxCounter_;
|
||||
std::atomic_uint64_t unknownCommandCounter_;
|
||||
std::atomic_uint64_t internalErrorCounter_;
|
||||
CounterType tooBusyCounter_;
|
||||
CounterType notReadyCounter_;
|
||||
CounterType badSyntaxCounter_;
|
||||
CounterType unknownCommandCounter_;
|
||||
CounterType internalErrorCounter_;
|
||||
|
||||
std::reference_wrapper<WorkQueue const> workQueue_;
|
||||
std::chrono::time_point<std::chrono::system_clock> startupTime_;
|
||||
@@ -66,7 +73,7 @@ public:
|
||||
*
|
||||
* @param wq The work queue to operate on
|
||||
*/
|
||||
Counters(WorkQueue const& wq) : workQueue_(std::cref(wq)), startupTime_{std::chrono::system_clock::now()} {};
|
||||
Counters(WorkQueue const& wq);
|
||||
|
||||
/**
|
||||
* @brief A factory function that creates a new counters instance.
|
||||
|
||||
@@ -21,13 +21,35 @@
|
||||
|
||||
namespace rpc {
|
||||
|
||||
WorkQueue::WorkQueue(std::uint32_t numWorkers, uint32_t maxSize) : ioc_{numWorkers}
|
||||
WorkQueue::WorkQueue(std::uint32_t numWorkers, uint32_t maxSize)
|
||||
: queued_{PrometheusService::counterInt(
|
||||
"work_queue_queued_total_number",
|
||||
util::prometheus::Labels(),
|
||||
"The total number of tasks queued for processing"
|
||||
)}
|
||||
, durationUs_{PrometheusService::counterInt(
|
||||
"work_queue_cumulitive_tasks_duration_us",
|
||||
util::prometheus::Labels(),
|
||||
"The total number of microseconds tasks were waiting to be executed"
|
||||
)}
|
||||
, curSize_{PrometheusService::gaugeInt(
|
||||
"work_queue_current_size",
|
||||
util::prometheus::Labels(),
|
||||
"The current number of tasks in the queue"
|
||||
)}
|
||||
, ioc_{numWorkers}
|
||||
{
|
||||
if (maxSize != 0)
|
||||
maxSize_ = maxSize;
|
||||
}
|
||||
|
||||
WorkQueue::~WorkQueue()
|
||||
{
|
||||
join();
|
||||
}
|
||||
|
||||
void
|
||||
WorkQueue::join()
|
||||
{
|
||||
ioc_.join();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <util/config/Config.h>
|
||||
#include <util/log/Logger.h>
|
||||
#include <util/prometheus/Prometheus.h>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
@@ -39,10 +40,10 @@ namespace rpc {
|
||||
*/
|
||||
class WorkQueue {
|
||||
// these are cumulative for the lifetime of the process
|
||||
std::atomic_uint64_t queued_ = 0;
|
||||
std::atomic_uint64_t durationUs_ = 0;
|
||||
std::reference_wrapper<util::prometheus::CounterInt> queued_;
|
||||
std::reference_wrapper<util::prometheus::CounterInt> durationUs_;
|
||||
|
||||
std::atomic_uint64_t curSize_ = 0;
|
||||
std::reference_wrapper<util::prometheus::GaugeInt> curSize_;
|
||||
uint32_t maxSize_ = std::numeric_limits<uint32_t>::max();
|
||||
|
||||
util::Logger log_{"RPC"};
|
||||
@@ -89,13 +90,13 @@ public:
|
||||
bool
|
||||
postCoro(FnType&& func, bool isWhiteListed)
|
||||
{
|
||||
if (curSize_ >= maxSize_ && !isWhiteListed) {
|
||||
LOG(log_.warn()) << "Queue is full. rejecting job. current size = " << curSize_
|
||||
if (curSize_.get().value() >= maxSize_ && !isWhiteListed) {
|
||||
LOG(log_.warn()) << "Queue is full. rejecting job. current size = " << curSize_.get().value()
|
||||
<< "; max size = " << maxSize_;
|
||||
return false;
|
||||
}
|
||||
|
||||
++curSize_;
|
||||
++curSize_.get();
|
||||
|
||||
// Each time we enqueue a job, we want to post a symmetrical job that will dequeue and run the job at the front
|
||||
// of the job queue.
|
||||
@@ -105,12 +106,12 @@ public:
|
||||
auto const run = std::chrono::system_clock::now();
|
||||
auto const wait = std::chrono::duration_cast<std::chrono::microseconds>(run - start).count();
|
||||
|
||||
++queued_;
|
||||
durationUs_ += wait;
|
||||
LOG(log_.info()) << "WorkQueue wait time = " << wait << " queue size = " << curSize_;
|
||||
++queued_.get();
|
||||
durationUs_.get() += wait;
|
||||
LOG(log_.info()) << "WorkQueue wait time = " << wait << " queue size = " << curSize_.get().value();
|
||||
|
||||
func(yield);
|
||||
--curSize_;
|
||||
--curSize_.get();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -127,13 +128,19 @@ public:
|
||||
{
|
||||
auto obj = boost::json::object{};
|
||||
|
||||
obj["queued"] = queued_;
|
||||
obj["queued_duration_us"] = durationUs_;
|
||||
obj["current_queue_size"] = curSize_;
|
||||
obj["queued"] = queued_.get().value();
|
||||
obj["queued_duration_us"] = durationUs_.get().value();
|
||||
obj["current_queue_size"] = curSize_.get().value();
|
||||
obj["max_queue_size"] = maxSize_;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wait until all the jobs in the queue are finished.
|
||||
*/
|
||||
void
|
||||
join();
|
||||
};
|
||||
|
||||
} // namespace rpc
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
105
src/util/prometheus/Counter.h
Normal file
105
src/util/prometheus/Counter.h
Normal file
@@ -0,0 +1,105 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <util/prometheus/Metrics.h>
|
||||
#include <util/prometheus/impl/AnyCounterBase.h>
|
||||
|
||||
namespace util::prometheus {
|
||||
|
||||
/**
|
||||
* @brief A prometheus counter metric implementation. It can only be increased or be reset to zero.
|
||||
*/
|
||||
template <detail::SomeNumberType NumberType>
|
||||
struct AnyCounter : MetricBase, detail::AnyCounterBase<NumberType> {
|
||||
using ValueType = NumberType;
|
||||
|
||||
/**
|
||||
* @brief Construct a new AnyCounter object
|
||||
*
|
||||
* @param name The name of the counter
|
||||
* @param labelsString The labels of the counter
|
||||
* @param impl The implementation of the counter
|
||||
*/
|
||||
template <detail::SomeCounterImpl ImplType = detail::CounterImpl<ValueType>>
|
||||
requires std::same_as<ValueType, typename std::remove_cvref_t<ImplType>::ValueType>
|
||||
AnyCounter(std::string name, std::string labelsString, ImplType&& impl = ImplType{})
|
||||
: MetricBase(std::move(name), std::move(labelsString))
|
||||
, detail::AnyCounterBase<ValueType>(std::forward<ImplType>(impl))
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Increase the counter by one
|
||||
*/
|
||||
AnyCounter&
|
||||
operator++()
|
||||
{
|
||||
this->pimpl_->add(ValueType{1});
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Increase the counter by the given value
|
||||
*
|
||||
* @param value The value to increase the counter by
|
||||
*/
|
||||
AnyCounter&
|
||||
operator+=(ValueType const value)
|
||||
{
|
||||
assert(value >= 0);
|
||||
this->pimpl_->add(value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reset the counter to zero
|
||||
*/
|
||||
void
|
||||
reset()
|
||||
{
|
||||
this->pimpl_->set(ValueType{0});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the value of the counter
|
||||
*/
|
||||
ValueType
|
||||
value() const
|
||||
{
|
||||
return this->pimpl_->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Serialize the counter to a string in prometheus format (i.e. name{labels} value)
|
||||
*
|
||||
* @param result The string to serialize into
|
||||
*/
|
||||
void
|
||||
serializeValue(std::string& result) const override
|
||||
{
|
||||
fmt::format_to(std::back_inserter(result), "{}{} {}", this->name(), this->labelsString(), value());
|
||||
}
|
||||
};
|
||||
|
||||
using CounterInt = AnyCounter<std::uint64_t>;
|
||||
using CounterDouble = AnyCounter<double>;
|
||||
|
||||
} // namespace util::prometheus
|
||||
128
src/util/prometheus/Gauge.h
Normal file
128
src/util/prometheus/Gauge.h
Normal file
@@ -0,0 +1,128 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <util/prometheus/Metrics.h>
|
||||
#include <util/prometheus/impl/AnyCounterBase.h>
|
||||
|
||||
namespace util::prometheus {
|
||||
|
||||
/**
|
||||
* @brief A prometheus gauge metric implementation. It can be increased, decreased or set to a value.
|
||||
*/
|
||||
template <detail::SomeNumberType NumberType>
|
||||
struct AnyGauge : MetricBase, detail::AnyCounterBase<NumberType> {
|
||||
using ValueType = NumberType;
|
||||
|
||||
/**
|
||||
* @brief Construct a new AnyGauge object
|
||||
*
|
||||
* @param name The name of the gauge
|
||||
* @param labelsString The labels of the gauge
|
||||
* @param impl The implementation of the counter inside the gauge
|
||||
*/
|
||||
template <detail::SomeCounterImpl ImplType = detail::CounterImpl<ValueType>>
|
||||
requires std::same_as<ValueType, typename std::remove_cvref_t<ImplType>::ValueType>
|
||||
AnyGauge(std::string name, std::string labelsString, ImplType&& impl = ImplType{})
|
||||
: MetricBase(std::move(name), std::move(labelsString))
|
||||
, detail::AnyCounterBase<ValueType>(std::forward<ImplType>(impl))
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Increase the gauge by one
|
||||
*/
|
||||
AnyGauge&
|
||||
operator++()
|
||||
{
|
||||
this->pimpl_->add(ValueType{1});
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Decrease the gauge by one
|
||||
*/
|
||||
AnyGauge&
|
||||
operator--()
|
||||
{
|
||||
this->pimpl_->add(ValueType{-1});
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Increase the gauge by the given value
|
||||
*
|
||||
* @param value The value to increase the gauge by
|
||||
*/
|
||||
AnyGauge&
|
||||
operator+=(ValueType const value)
|
||||
{
|
||||
this->pimpl_->add(value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Decrease the gauge by the given value
|
||||
*
|
||||
* @param value The value to decrease the gauge by
|
||||
*/
|
||||
AnyGauge&
|
||||
operator-=(ValueType const value)
|
||||
{
|
||||
this->pimpl_->add(-value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the gauge to the given value
|
||||
*
|
||||
* @param value The value to set the gauge to
|
||||
*/
|
||||
void
|
||||
set(ValueType const value)
|
||||
{
|
||||
this->pimpl_->set(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the value of the counter
|
||||
*/
|
||||
ValueType
|
||||
value() const
|
||||
{
|
||||
return this->pimpl_->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Serialize the counter to a string in prometheus format (i.e. name{labels} value)
|
||||
*
|
||||
* @param result The string to serialize into
|
||||
*/
|
||||
void
|
||||
serializeValue(std::string& result) const override
|
||||
{
|
||||
fmt::format_to(std::back_inserter(result), "{}{} {}", this->name(), this->labelsString(), value());
|
||||
}
|
||||
};
|
||||
|
||||
using GaugeInt = AnyGauge<std::int64_t>;
|
||||
using GaugeDouble = AnyGauge<double>;
|
||||
|
||||
} // namespace util::prometheus
|
||||
62
src/util/prometheus/Http.cpp
Normal file
62
src/util/prometheus/Http.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <util/prometheus/Http.h>
|
||||
|
||||
namespace util::prometheus {
|
||||
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
namespace {
|
||||
|
||||
bool
|
||||
isPrometheusRequest(http::request<http::string_body> const& req)
|
||||
{
|
||||
return req.method() == http::verb::get && req.target() == "/metrics";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<http::response<http::string_body>>
|
||||
handlePrometheusRequest(http::request<http::string_body> const& req, bool const isAdmin)
|
||||
{
|
||||
bool const prometheusRequest = isPrometheusRequest(req);
|
||||
|
||||
if (!prometheusRequest)
|
||||
return std::nullopt;
|
||||
|
||||
if (!isAdmin) {
|
||||
return http::response<http::string_body>(
|
||||
http::status::unauthorized, req.version(), "Only admin is allowed to collect metrics"
|
||||
);
|
||||
}
|
||||
|
||||
if (not PrometheusService::isEnabled()) {
|
||||
return http::response<http::string_body>(
|
||||
http::status::forbidden, req.version(), "Prometheus is disabled in clio config"
|
||||
);
|
||||
}
|
||||
|
||||
auto response = http::response<http::string_body>(http::status::ok, req.version());
|
||||
response.set(http::field::content_type, "text/plain; version=0.0.4");
|
||||
response.body() = PrometheusService::collectMetrics(); // TODO(#932): add gzip compression
|
||||
return response;
|
||||
}
|
||||
|
||||
} // namespace util::prometheus
|
||||
38
src/util/prometheus/Http.h
Normal file
38
src/util/prometheus/Http.h
Normal file
@@ -0,0 +1,38 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <util/prometheus/Prometheus.h>
|
||||
|
||||
#include <boost/beast/http.hpp>
|
||||
|
||||
namespace util::prometheus {
|
||||
|
||||
/**
|
||||
* @brief Handles a prometheus request
|
||||
*
|
||||
* @param req The http request from primetheus (required only to reply with the same http version)
|
||||
* @param isAdmin Whether the request is from an admin
|
||||
* @return nullopt if the request shouldn't be handled, respoce for Prometheus otherwise
|
||||
*/
|
||||
std::optional<boost::beast::http::response<boost::beast::http::string_body>>
|
||||
handlePrometheusRequest(boost::beast::http::request<boost::beast::http::string_body> const& req, bool isAdmin);
|
||||
|
||||
} // namespace util::prometheus
|
||||
92
src/util/prometheus/Label.cpp
Normal file
92
src/util/prometheus/Label.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <util/prometheus/Label.h>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace util::prometheus {
|
||||
|
||||
Label::Label(std::string name, std::string value) : name_(std::move(name)), value_(std::move(value))
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
Label::operator<(Label const& rhs) const
|
||||
{
|
||||
return std::tie(name_, value_) < std::tie(rhs.name_, rhs.value_);
|
||||
}
|
||||
|
||||
bool
|
||||
Label::operator==(Label const& rhs) const
|
||||
{
|
||||
return std::tie(name_, value_) == std::tie(rhs.name_, rhs.value_);
|
||||
}
|
||||
|
||||
std::string
|
||||
Label::serialize() const
|
||||
{
|
||||
std::string escapedValue;
|
||||
escapedValue.reserve(value_.size());
|
||||
for (auto const c : value_) {
|
||||
switch (c) {
|
||||
case '\n': {
|
||||
escapedValue.push_back('\\');
|
||||
escapedValue.push_back('n');
|
||||
break;
|
||||
}
|
||||
case '\\':
|
||||
[[fallthrough]];
|
||||
case '"': {
|
||||
escapedValue.push_back('\\');
|
||||
[[fallthrough]];
|
||||
}
|
||||
default:
|
||||
escapedValue.push_back(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fmt::format("{}=\"{}\"", name_, std::move(escapedValue));
|
||||
}
|
||||
|
||||
Labels::Labels(std::vector<Label> labels) : labels_(std::move(labels))
|
||||
{
|
||||
std::sort(labels_.begin(), labels_.end());
|
||||
}
|
||||
|
||||
std::string
|
||||
Labels::serialize() const
|
||||
{
|
||||
std::string result;
|
||||
|
||||
if (!labels_.empty())
|
||||
result.push_back('{');
|
||||
|
||||
for (auto& label : labels_) {
|
||||
result += label.serialize();
|
||||
result.push_back(',');
|
||||
}
|
||||
|
||||
if (!result.empty())
|
||||
result.back() = '}';
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace util::prometheus
|
||||
73
src/util/prometheus/Label.h
Normal file
73
src/util/prometheus/Label.h
Normal file
@@ -0,0 +1,73 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace util::prometheus {
|
||||
|
||||
/**
|
||||
* @brief Class representing a Prometheus label
|
||||
*/
|
||||
class Label {
|
||||
public:
|
||||
Label(std::string name, std::string value);
|
||||
|
||||
bool
|
||||
operator<(Label const& rhs) const;
|
||||
|
||||
bool
|
||||
operator==(Label const& rhs) const;
|
||||
|
||||
/**
|
||||
* @brief Serialize the label to a string in Prometheus format (e.g. name="value"). The value is escaped
|
||||
*
|
||||
* @return The serialized label
|
||||
*/
|
||||
std::string
|
||||
serialize() const;
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::string value_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Class representing a collection of Prometheus labels
|
||||
*/
|
||||
class Labels {
|
||||
public:
|
||||
Labels() = default;
|
||||
explicit Labels(std::vector<Label> labels);
|
||||
|
||||
/**
|
||||
* @brief Serialize the labels to a string in Prometheus format (e.g. {"name1="value1",name2="value2"})
|
||||
*
|
||||
* @return The serialized labels
|
||||
*/
|
||||
std::string
|
||||
serialize() const;
|
||||
|
||||
private:
|
||||
std::vector<Label> labels_;
|
||||
};
|
||||
|
||||
} // namespace util::prometheus
|
||||
142
src/util/prometheus/Metrics.cpp
Normal file
142
src/util/prometheus/Metrics.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <util/prometheus/Counter.h>
|
||||
#include <util/prometheus/Gauge.h>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace util::prometheus {
|
||||
|
||||
MetricBase::MetricBase(std::string name, std::string labelsString)
|
||||
: name_(std::move(name)), labelsString_(std::move(labelsString))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
MetricBase::serialize(std::string& s) const
|
||||
{
|
||||
serializeValue(s);
|
||||
}
|
||||
|
||||
char const*
|
||||
toString(MetricType type)
|
||||
{
|
||||
switch (type) {
|
||||
case MetricType::COUNTER_INT:
|
||||
[[fallthrough]];
|
||||
case MetricType::COUNTER_DOUBLE:
|
||||
return "counter";
|
||||
case MetricType::GAUGE_INT:
|
||||
[[fallthrough]];
|
||||
case MetricType::GAUGE_DOUBLE:
|
||||
return "gauge";
|
||||
case MetricType::HISTOGRAM:
|
||||
return "histogram";
|
||||
case MetricType::SUMMARY:
|
||||
return "summary";
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string const&
|
||||
MetricBase::name() const
|
||||
{
|
||||
return name_;
|
||||
}
|
||||
|
||||
std::string const&
|
||||
MetricBase::labelsString() const
|
||||
{
|
||||
return labelsString_;
|
||||
}
|
||||
|
||||
MetricsFamily::MetricBuilder MetricsFamily::defaultMetricBuilder =
|
||||
[](std::string name, std::string labelsString, MetricType type) -> std::unique_ptr<MetricBase> {
|
||||
switch (type) {
|
||||
case MetricType::COUNTER_INT:
|
||||
return std::make_unique<CounterInt>(name, labelsString);
|
||||
case MetricType::COUNTER_DOUBLE:
|
||||
return std::make_unique<CounterDouble>(name, labelsString);
|
||||
case MetricType::GAUGE_INT:
|
||||
return std::make_unique<GaugeInt>(name, labelsString);
|
||||
case MetricType::GAUGE_DOUBLE:
|
||||
return std::make_unique<GaugeDouble>(name, labelsString);
|
||||
case MetricType::SUMMARY:
|
||||
[[fallthrough]];
|
||||
case MetricType::HISTOGRAM:
|
||||
[[fallthrough]];
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
MetricsFamily::MetricsFamily(
|
||||
std::string name,
|
||||
std::optional<std::string> description,
|
||||
MetricType type,
|
||||
MetricBuilder& metricBuilder
|
||||
)
|
||||
: name_(std::move(name)), description_(std::move(description)), type_(type), metricBuilder_(metricBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
MetricBase&
|
||||
MetricsFamily::getMetric(Labels labels)
|
||||
{
|
||||
auto labelsString = labels.serialize();
|
||||
auto it = metrics_.find(labelsString);
|
||||
if (it == metrics_.end()) {
|
||||
auto metric = metricBuilder_(name(), labelsString, type());
|
||||
auto [it2, success] = metrics_.emplace(std::move(labelsString), std::move(metric));
|
||||
it = it2;
|
||||
}
|
||||
return *it->second;
|
||||
}
|
||||
|
||||
void
|
||||
MetricsFamily::serialize(std::string& result) const
|
||||
{
|
||||
if (description_)
|
||||
fmt::format_to(std::back_inserter(result), "# HELP {} {}\n", name_, *description_);
|
||||
fmt::format_to(std::back_inserter(result), "# TYPE {} {}\n", name_, toString(type()));
|
||||
|
||||
for (auto const& [labelsString, metric] : metrics_) {
|
||||
metric->serialize(result);
|
||||
result.push_back('\n');
|
||||
}
|
||||
result.push_back('\n');
|
||||
}
|
||||
|
||||
std::string const&
|
||||
MetricsFamily::name() const
|
||||
{
|
||||
return name_;
|
||||
}
|
||||
|
||||
MetricType
|
||||
MetricsFamily::type() const
|
||||
{
|
||||
return type_;
|
||||
}
|
||||
|
||||
} // namespace util::prometheus
|
||||
140
src/util/prometheus/Metrics.h
Normal file
140
src/util/prometheus/Metrics.h
Normal file
@@ -0,0 +1,140 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <util/prometheus/Label.h>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace util::prometheus {
|
||||
|
||||
/**
|
||||
* @brief Base class for a Prometheus metric containing a name and labels
|
||||
*/
|
||||
class MetricBase {
|
||||
public:
|
||||
MetricBase(std::string name, std::string labelsString);
|
||||
|
||||
MetricBase(MetricBase const&) = delete;
|
||||
MetricBase(MetricBase&&) = default;
|
||||
MetricBase&
|
||||
operator=(MetricBase const&) = delete;
|
||||
MetricBase&
|
||||
operator=(MetricBase&&) = default;
|
||||
virtual ~MetricBase() = default;
|
||||
|
||||
/**
|
||||
* @brief Serialize the metric to a string in Prometheus format
|
||||
*
|
||||
* @param s The string to serialize into
|
||||
*/
|
||||
void
|
||||
serialize(std::string& s) const;
|
||||
|
||||
/**
|
||||
* @brief Get the name of the metric
|
||||
*/
|
||||
std::string const&
|
||||
name() const;
|
||||
|
||||
/**
|
||||
* @brief Get the labels of the metric in serialized format, e.g. {name="value",name2="value2"}
|
||||
*/
|
||||
std::string const&
|
||||
labelsString() const;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Interface to serialize the value of the metric
|
||||
*
|
||||
* @param result The string to serialize into
|
||||
*/
|
||||
virtual void
|
||||
serializeValue(std::string& result) const = 0;
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::string labelsString_;
|
||||
};
|
||||
|
||||
enum class MetricType { COUNTER_INT, COUNTER_DOUBLE, GAUGE_INT, GAUGE_DOUBLE, HISTOGRAM, SUMMARY };
|
||||
|
||||
char const*
|
||||
toString(MetricType type);
|
||||
|
||||
/**
|
||||
* @brief Class representing a collection of Prometheus metric with the same name and type
|
||||
*/
|
||||
class MetricsFamily {
|
||||
public:
|
||||
using MetricBuilder = std::function<std::unique_ptr<MetricBase>(std::string, std::string, MetricType)>;
|
||||
static MetricBuilder defaultMetricBuilder;
|
||||
|
||||
MetricsFamily(
|
||||
std::string name,
|
||||
std::optional<std::string> description,
|
||||
MetricType type,
|
||||
MetricBuilder& builder = defaultMetricBuilder
|
||||
);
|
||||
|
||||
MetricsFamily(MetricsFamily const&) = delete;
|
||||
MetricsFamily(MetricsFamily&&) = default;
|
||||
MetricsFamily&
|
||||
operator=(MetricsFamily const&) = delete;
|
||||
MetricsFamily&
|
||||
operator=(MetricsFamily&&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Get the metric with the given labels. If it does not exist, it will be created
|
||||
*
|
||||
* @param labels The labels of the metric
|
||||
* @return Reference to the metric
|
||||
*/
|
||||
MetricBase&
|
||||
getMetric(Labels labels);
|
||||
|
||||
/**
|
||||
* @brief Serialize all the containing metrics to a string in Prometheus format as one block
|
||||
*
|
||||
* @param result The string to serialize into
|
||||
*/
|
||||
void
|
||||
serialize(std::string& result) const;
|
||||
|
||||
std::string const&
|
||||
name() const;
|
||||
|
||||
MetricType
|
||||
type() const;
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::optional<std::string> description_;
|
||||
std::unordered_map<std::string, std::unique_ptr<MetricBase>> metrics_;
|
||||
MetricType type_;
|
||||
MetricBuilder& metricBuilder_;
|
||||
};
|
||||
|
||||
} // namespace util::prometheus
|
||||
169
src/util/prometheus/Prometheus.cpp
Normal file
169
src/util/prometheus/Prometheus.cpp
Normal file
@@ -0,0 +1,169 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <util/prometheus/Prometheus.h>
|
||||
|
||||
namespace util::prometheus {
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename MetricType>
|
||||
MetricType&
|
||||
convertBaseTo(MetricBase& metricBase)
|
||||
{
|
||||
auto result = dynamic_cast<MetricType*>(&metricBase);
|
||||
assert(result != nullptr);
|
||||
if (result == nullptr)
|
||||
throw std::runtime_error("Failed to convert metric type");
|
||||
return *result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CounterInt&
|
||||
PrometheusImpl::counterInt(std::string name, Labels labels, std::optional<std::string> description)
|
||||
{
|
||||
MetricBase& metricBase =
|
||||
getMetric(std::move(name), std::move(labels), std::move(description), MetricType::COUNTER_INT);
|
||||
return convertBaseTo<CounterInt>(metricBase);
|
||||
}
|
||||
|
||||
CounterDouble&
|
||||
PrometheusImpl::counterDouble(std::string name, Labels labels, std::optional<std::string> description)
|
||||
{
|
||||
MetricBase& metricBase =
|
||||
getMetric(std::move(name), std::move(labels), std::move(description), MetricType::COUNTER_DOUBLE);
|
||||
return convertBaseTo<CounterDouble>(metricBase);
|
||||
}
|
||||
|
||||
GaugeInt&
|
||||
PrometheusImpl::gaugeInt(std::string name, Labels labels, std::optional<std::string> description)
|
||||
{
|
||||
MetricBase& metricBase =
|
||||
getMetric(std::move(name), std::move(labels), std::move(description), MetricType::GAUGE_INT);
|
||||
return convertBaseTo<GaugeInt>(metricBase);
|
||||
}
|
||||
|
||||
GaugeDouble&
|
||||
PrometheusImpl::gaugeDouble(std::string name, Labels labels, std::optional<std::string> description)
|
||||
{
|
||||
MetricBase& metricBase =
|
||||
getMetric(std::move(name), std::move(labels), std::move(description), MetricType::GAUGE_DOUBLE);
|
||||
return convertBaseTo<GaugeDouble>(metricBase);
|
||||
}
|
||||
|
||||
std::string
|
||||
PrometheusImpl::collectMetrics()
|
||||
{
|
||||
std::string result;
|
||||
|
||||
if (!isEnabled())
|
||||
return result;
|
||||
|
||||
for (auto const& [name, family] : metrics_) {
|
||||
family.serialize(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
MetricBase&
|
||||
PrometheusImpl::getMetric(
|
||||
std::string name,
|
||||
Labels labels,
|
||||
std::optional<std::string> description,
|
||||
MetricType const type
|
||||
)
|
||||
{
|
||||
auto it = metrics_.find(name);
|
||||
if (it == metrics_.end()) {
|
||||
auto nameCopy = name;
|
||||
it = metrics_.emplace(std::move(nameCopy), MetricsFamily(std::move(name), std::move(description), type)).first;
|
||||
} else if (it->second.type() != type) {
|
||||
throw std::runtime_error("Metrics of different type can't have the same name: " + name);
|
||||
}
|
||||
return it->second.getMetric(std::move(labels));
|
||||
}
|
||||
|
||||
} // namespace util::prometheus
|
||||
|
||||
void
|
||||
PrometheusService::init(util::Config const& config)
|
||||
{
|
||||
bool const enabled = config.valueOr("prometheus_enabled", true);
|
||||
instance_ = std::make_unique<util::prometheus::PrometheusImpl>(enabled);
|
||||
}
|
||||
|
||||
util::prometheus::CounterInt&
|
||||
PrometheusService::counterInt(std::string name, util::prometheus::Labels labels, std::optional<std::string> description)
|
||||
{
|
||||
return instance().counterInt(std::move(name), std::move(labels), std::move(description));
|
||||
}
|
||||
|
||||
util::prometheus::CounterDouble&
|
||||
PrometheusService::counterDouble(
|
||||
std::string name,
|
||||
util::prometheus::Labels labels,
|
||||
std::optional<std::string> description
|
||||
)
|
||||
{
|
||||
return instance().counterDouble(std::move(name), std::move(labels), std::move(description));
|
||||
}
|
||||
|
||||
util::prometheus::GaugeInt&
|
||||
PrometheusService::gaugeInt(std::string name, util::prometheus::Labels labels, std::optional<std::string> description)
|
||||
{
|
||||
return instance().gaugeInt(std::move(name), std::move(labels), std::move(description));
|
||||
}
|
||||
|
||||
util::prometheus::GaugeDouble&
|
||||
PrometheusService::gaugeDouble(
|
||||
std::string name,
|
||||
util::prometheus::Labels labels,
|
||||
std::optional<std::string> description
|
||||
)
|
||||
{
|
||||
return instance().gaugeDouble(std::move(name), std::move(labels), std::move(description));
|
||||
}
|
||||
|
||||
std::string
|
||||
PrometheusService::collectMetrics()
|
||||
{
|
||||
return instance().collectMetrics();
|
||||
}
|
||||
|
||||
bool
|
||||
PrometheusService::isEnabled()
|
||||
{
|
||||
return instance().isEnabled();
|
||||
}
|
||||
|
||||
void
|
||||
PrometheusService::replaceInstance(std::unique_ptr<util::prometheus::PrometheusInterface> instance)
|
||||
{
|
||||
instance_ = std::move(instance);
|
||||
}
|
||||
|
||||
util::prometheus::PrometheusInterface&
|
||||
PrometheusService::instance()
|
||||
{
|
||||
assert(instance_);
|
||||
return *instance_;
|
||||
}
|
||||
|
||||
std::unique_ptr<util::prometheus::PrometheusInterface> PrometheusService::instance_;
|
||||
239
src/util/prometheus/Prometheus.h
Normal file
239
src/util/prometheus/Prometheus.h
Normal file
@@ -0,0 +1,239 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <util/config/Config.h>
|
||||
#include <util/prometheus/Counter.h>
|
||||
#include <util/prometheus/Gauge.h>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace util::prometheus {
|
||||
|
||||
class PrometheusInterface {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Prometheus Interface object
|
||||
*
|
||||
* @param isEnabled Whether prometheus is enabled
|
||||
*/
|
||||
PrometheusInterface(bool isEnabled) : isEnabled_(isEnabled)
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~PrometheusInterface() = default;
|
||||
|
||||
/**
|
||||
* @brief Get a integer based counter metric. It will be created if it doesn't exist
|
||||
*
|
||||
* @param name The name of the metric
|
||||
* @param labels The labels of the metric
|
||||
* @param description The description of the metric
|
||||
*/
|
||||
virtual CounterInt&
|
||||
counterInt(std::string name, Labels labels, std::optional<std::string> description = std::nullopt) = 0;
|
||||
|
||||
/**
|
||||
* @brief Get a double based counter metric. It will be created if it doesn't exist
|
||||
*
|
||||
* @param name The name of the metric
|
||||
* @param labels The labels of the metric
|
||||
* @param description The description of the metric
|
||||
*/
|
||||
virtual CounterDouble&
|
||||
counterDouble(std::string name, Labels labels, std::optional<std::string> description = std::nullopt) = 0;
|
||||
|
||||
/**
|
||||
* @brief Get a integer based gauge metric. It will be created if it doesn't exist
|
||||
*
|
||||
* @param name The name of the metric
|
||||
* @param labels The labels of the metric
|
||||
* @param description The description of the metric
|
||||
*/
|
||||
virtual GaugeInt&
|
||||
gaugeInt(std::string name, Labels labels, std::optional<std::string> description = std::nullopt) = 0;
|
||||
|
||||
/**
|
||||
* @brief Get a double based gauge metric. It will be created if it doesn't exist
|
||||
*
|
||||
* @param name The name of the metric
|
||||
* @param labels The labels of the metric
|
||||
* @param description The description of the metric
|
||||
*/
|
||||
virtual GaugeDouble&
|
||||
gaugeDouble(std::string name, Labels labels, std::optional<std::string> description = std::nullopt) = 0;
|
||||
|
||||
/**
|
||||
* @brief Collect all metrics and return them as a string in Prometheus format
|
||||
*
|
||||
* @return The serialized metrics
|
||||
*/
|
||||
virtual std::string
|
||||
collectMetrics() = 0;
|
||||
|
||||
/**
|
||||
* @brief Whether prometheus is enabled
|
||||
*
|
||||
* @return true if prometheus is enabled
|
||||
*/
|
||||
bool
|
||||
isEnabled() const
|
||||
{
|
||||
return isEnabled_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool isEnabled_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Implemetation of PrometheusInterface
|
||||
*
|
||||
* @note When prometheus is disabled, all metrics will still counted but collection is disabled
|
||||
*/
|
||||
class PrometheusImpl : public PrometheusInterface {
|
||||
public:
|
||||
using PrometheusInterface::PrometheusInterface;
|
||||
|
||||
CounterInt&
|
||||
counterInt(std::string name, Labels labels, std::optional<std::string> description) override;
|
||||
|
||||
CounterDouble&
|
||||
counterDouble(std::string name, Labels labels, std::optional<std::string> description) override;
|
||||
|
||||
GaugeInt&
|
||||
gaugeInt(std::string name, Labels labels, std::optional<std::string> description) override;
|
||||
|
||||
GaugeDouble&
|
||||
gaugeDouble(std::string name, Labels labels, std::optional<std::string> description) override;
|
||||
|
||||
std::string
|
||||
collectMetrics() override;
|
||||
|
||||
private:
|
||||
MetricBase&
|
||||
getMetric(std::string name, Labels labels, std::optional<std::string> description, MetricType type);
|
||||
|
||||
std::unordered_map<std::string, MetricsFamily> metrics_;
|
||||
};
|
||||
|
||||
} // namespace util::prometheus
|
||||
|
||||
/**
|
||||
* @brief Singleton class to access the PrometheusInterface
|
||||
*/
|
||||
class PrometheusService {
|
||||
public:
|
||||
/**
|
||||
* @brief Initialize the singleton with the given configuration
|
||||
*
|
||||
* @param config The configuration to use
|
||||
*/
|
||||
void static init(util::Config const& config = util::Config{});
|
||||
|
||||
/**
|
||||
* @brief Get a integer based counter metric. It will be created if it doesn't exist
|
||||
*
|
||||
* @param name The name of the metric
|
||||
* @param labels The labels of the metric
|
||||
* @param description The description of the metric
|
||||
*/
|
||||
static util::prometheus::CounterInt&
|
||||
counterInt(
|
||||
std::string name,
|
||||
util::prometheus::Labels labels,
|
||||
std::optional<std::string> description = std::nullopt
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Get a double based counter metric. It will be created if it doesn't exist
|
||||
*
|
||||
* @param name The name of the metric
|
||||
* @param labels The labels of the metric
|
||||
* @param description The description of the metric
|
||||
*/
|
||||
static util::prometheus::CounterDouble&
|
||||
counterDouble(
|
||||
std::string name,
|
||||
util::prometheus::Labels labels,
|
||||
std::optional<std::string> description = std::nullopt
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Get a integer based gauge metric. It will be created if it doesn't exist
|
||||
*
|
||||
* @param name The name of the metric
|
||||
* @param labels The labels of the metric
|
||||
* @param description The description of the metric
|
||||
*/
|
||||
static util::prometheus::GaugeInt&
|
||||
gaugeInt(std::string name, util::prometheus::Labels labels, std::optional<std::string> description = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief Get a double based gauge metric. It will be created if it doesn't exist
|
||||
*
|
||||
* @param name The name of the metric
|
||||
* @param labels The labels of the metric
|
||||
* @param description The description of the metric
|
||||
*/
|
||||
static util::prometheus::GaugeDouble&
|
||||
gaugeDouble(
|
||||
std::string name,
|
||||
util::prometheus::Labels labels,
|
||||
std::optional<std::string> description = std::nullopt
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Collect all metrics and return them as a string in Prometheus format
|
||||
*
|
||||
* @return The serialized metrics
|
||||
*/
|
||||
static std::string
|
||||
collectMetrics();
|
||||
|
||||
/**
|
||||
* @brief Whether prometheus is enabled
|
||||
*
|
||||
* @return true if prometheus is enabled
|
||||
*/
|
||||
static bool
|
||||
isEnabled();
|
||||
|
||||
/**
|
||||
* @brief Replace the prometheus object stored in the singleton
|
||||
*
|
||||
* @note Be careful with this method because there could be hanging references to counters
|
||||
*
|
||||
* @param instance The new prometheus object
|
||||
*/
|
||||
static void
|
||||
replaceInstance(std::unique_ptr<util::prometheus::PrometheusInterface> instance);
|
||||
|
||||
/**
|
||||
* @brief Get the prometheus object stored in the singleton
|
||||
*
|
||||
* @return The prometheus object reference
|
||||
*/
|
||||
static util::prometheus::PrometheusInterface&
|
||||
instance();
|
||||
|
||||
private:
|
||||
static std::unique_ptr<util::prometheus::PrometheusInterface> instance_;
|
||||
};
|
||||
82
src/util/prometheus/impl/AnyCounterBase.h
Normal file
82
src/util/prometheus/impl/AnyCounterBase.h
Normal file
@@ -0,0 +1,82 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <util/prometheus/impl/CounterImpl.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace util::prometheus::detail {
|
||||
|
||||
template <SomeNumberType NumberType>
|
||||
class AnyCounterBase {
|
||||
public:
|
||||
using ValueType = NumberType;
|
||||
|
||||
template <SomeCounterImpl ImplType = CounterImpl<ValueType>>
|
||||
requires std::same_as<ValueType, typename std::remove_cvref_t<ImplType>::ValueType>
|
||||
AnyCounterBase(ImplType&& impl = ImplType{})
|
||||
: pimpl_(std::make_unique<Model<ImplType>>(std::forward<ImplType>(impl)))
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
struct Concept {
|
||||
virtual ~Concept() = default;
|
||||
|
||||
virtual void add(ValueType) = 0;
|
||||
|
||||
virtual void set(ValueType) = 0;
|
||||
|
||||
virtual ValueType
|
||||
value() const = 0;
|
||||
};
|
||||
|
||||
template <SomeCounterImpl ImplType>
|
||||
struct Model : Concept {
|
||||
Model(ImplType impl) : impl_(std::forward<ImplType>(impl))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
add(ValueType value) override
|
||||
{
|
||||
impl_.add(value);
|
||||
}
|
||||
|
||||
void
|
||||
set(ValueType v) override
|
||||
{
|
||||
impl_.set(v);
|
||||
}
|
||||
|
||||
ValueType
|
||||
value() const override
|
||||
{
|
||||
return impl_.value();
|
||||
}
|
||||
|
||||
ImplType impl_;
|
||||
};
|
||||
|
||||
std::unique_ptr<Concept> pimpl_;
|
||||
};
|
||||
|
||||
} // namespace util::prometheus::detail
|
||||
101
src/util/prometheus/impl/CounterImpl.h
Normal file
101
src/util/prometheus/impl/CounterImpl.h
Normal file
@@ -0,0 +1,101 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <concepts>
|
||||
|
||||
namespace util::prometheus::detail {
|
||||
|
||||
template <typename T>
|
||||
concept SomeNumberType = std::is_arithmetic_v<T> && !std::is_same_v<T, bool> && !std::is_const_v<T>;
|
||||
|
||||
template <typename T>
|
||||
concept SomeCounterImpl = requires(T a) {
|
||||
typename std::remove_cvref_t<T>::ValueType;
|
||||
SomeNumberType<typename std::remove_cvref_t<T>::ValueType>;
|
||||
{
|
||||
a.add(typename std::remove_cvref_t<T>::ValueType{1})
|
||||
} -> std::same_as<void>;
|
||||
{
|
||||
a.set(typename std::remove_cvref_t<T>::ValueType{1})
|
||||
} -> std::same_as<void>;
|
||||
{
|
||||
a.value()
|
||||
} -> SomeNumberType;
|
||||
};
|
||||
|
||||
template <SomeNumberType NumberType>
|
||||
class CounterImpl {
|
||||
public:
|
||||
using ValueType = NumberType;
|
||||
|
||||
CounterImpl() = default;
|
||||
|
||||
CounterImpl(CounterImpl const&) = delete;
|
||||
|
||||
// Move constructor should be used only used during initialization
|
||||
CounterImpl(CounterImpl&& other)
|
||||
{
|
||||
assert(other.value_ == 0);
|
||||
value_ = other.value_.exchange(0);
|
||||
}
|
||||
|
||||
CounterImpl&
|
||||
operator=(CounterImpl const&) = delete;
|
||||
CounterImpl&
|
||||
operator=(CounterImpl&&) = delete;
|
||||
|
||||
void
|
||||
add(ValueType const value)
|
||||
{
|
||||
if constexpr (std::is_integral_v<ValueType>) {
|
||||
value_.fetch_add(value);
|
||||
} else {
|
||||
#if __cpp_lib_atomic_float >= 201711L
|
||||
value_.fetch_add(value);
|
||||
#else
|
||||
// Workaround for atomic float not being supported by the standard library
|
||||
// cimpares_exchange_weak returns false if the value is not exchanged and updates the current value
|
||||
auto current = value_.load();
|
||||
while (!value_.compare_exchange_weak(current, current + value)) {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
set(ValueType const value)
|
||||
{
|
||||
value_ = value;
|
||||
}
|
||||
|
||||
ValueType
|
||||
value() const
|
||||
{
|
||||
return value_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<ValueType> value_{0};
|
||||
};
|
||||
|
||||
} // namespace util::prometheus::detail
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <web/impl/AdminVerificationStrategy.h>
|
||||
|
||||
#include <ripple/protocol/digest.h>
|
||||
@@ -49,9 +50,16 @@ PasswordAdminVerificationStrategy::isAdmin(RequestType const& request, std::stri
|
||||
// No Authorization header
|
||||
return false;
|
||||
}
|
||||
std::string userAuth(it->value());
|
||||
std::transform(userAuth.begin(), userAuth.end(), userAuth.begin(), ::toupper);
|
||||
return passwordSha256_ == userAuth;
|
||||
auto userAuth = it->value();
|
||||
if (!userAuth.starts_with(passwordPrefix)) {
|
||||
// Invalid Authorization header
|
||||
return false;
|
||||
}
|
||||
userAuth.remove_prefix(passwordPrefix.size());
|
||||
std::string userPasswordHash;
|
||||
userPasswordHash.reserve(userAuth.size());
|
||||
std::transform(userAuth.begin(), userAuth.end(), std::back_inserter(userPasswordHash), ::toupper);
|
||||
return passwordSha256_ == userPasswordHash;
|
||||
}
|
||||
|
||||
std::shared_ptr<AdminVerificationStrategy>
|
||||
|
||||
@@ -60,6 +60,8 @@ private:
|
||||
std::string passwordSha256_;
|
||||
|
||||
public:
|
||||
static constexpr std::string_view passwordPrefix = "Password ";
|
||||
|
||||
PasswordAdminVerificationStrategy(std::string const& password);
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <main/Build.h>
|
||||
#include <rpc/Errors.h>
|
||||
#include <util/log/Logger.h>
|
||||
#include <util/prometheus/Http.h>
|
||||
#include <web/DOSGuard.h>
|
||||
#include <web/impl/AdminVerificationStrategy.h>
|
||||
#include <web/interface/Concepts.h>
|
||||
@@ -196,6 +198,9 @@ public:
|
||||
return sender_(httpResponse(http::status::too_many_requests, "text/html", "Too many requests"));
|
||||
}
|
||||
|
||||
if (auto response = util::prometheus::handlePrometheusRequest(req_, isAdmin()); response.has_value())
|
||||
return sender_(std::move(response.value()));
|
||||
|
||||
if (req_.method() != http::verb::post) {
|
||||
return sender_(httpResponse(http::status::bad_request, "text/html", "Expected a POST request"));
|
||||
}
|
||||
|
||||
@@ -16,12 +16,14 @@
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
#include <util/prometheus/Prometheus.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
int
|
||||
main(int argc, char** argv)
|
||||
{
|
||||
PrometheusService::init();
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
||||
@@ -20,26 +20,28 @@
|
||||
#include <feed/SubscriptionManager.h>
|
||||
|
||||
#include <util/Fixtures.h>
|
||||
#include <util/MockPrometheus.h>
|
||||
#include <util/MockWsBase.h>
|
||||
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
using namespace feed;
|
||||
using namespace util::prometheus;
|
||||
|
||||
// io_context
|
||||
class SubscriptionTest : public SyncAsioContextTest {
|
||||
protected:
|
||||
struct SubscriptionTestBase {
|
||||
util::Config cfg;
|
||||
util::TagDecoratorFactory tagDecoratorFactory{cfg};
|
||||
};
|
||||
|
||||
class SubscriptionMapTest : public SubscriptionTest {};
|
||||
struct SubscriptionTest : WithPrometheus, SyncAsioContextTest, SubscriptionTestBase {
|
||||
Subscription sub{ctx, "test"};
|
||||
};
|
||||
|
||||
// subscribe/unsubscribe the same session would not change the count
|
||||
TEST_F(SubscriptionTest, SubscriptionCount)
|
||||
{
|
||||
Subscription sub(ctx);
|
||||
std::shared_ptr<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
sub.subscribe(session1);
|
||||
@@ -73,7 +75,6 @@ TEST_F(SubscriptionTest, SubscriptionCount)
|
||||
// send interface will be called when publish called
|
||||
TEST_F(SubscriptionTest, SubscriptionPublish)
|
||||
{
|
||||
Subscription sub(ctx);
|
||||
std::shared_ptr<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
sub.subscribe(session1);
|
||||
@@ -102,7 +103,6 @@ TEST_F(SubscriptionTest, SubscriptionPublish)
|
||||
// when error happen during send(), the subsciber will be removed after
|
||||
TEST_F(SubscriptionTest, SubscriptionDeadRemoveSubscriber)
|
||||
{
|
||||
Subscription sub(ctx);
|
||||
std::shared_ptr<web::ConnectionBase> const session1(new MockDeadSession(tagDecoratorFactory));
|
||||
sub.subscribe(session1);
|
||||
ctx.run();
|
||||
@@ -118,12 +118,61 @@ TEST_F(SubscriptionTest, SubscriptionDeadRemoveSubscriber)
|
||||
EXPECT_EQ(sub.count(), 0);
|
||||
}
|
||||
|
||||
struct SubscriptionMockPrometheusTest : WithMockPrometheus, SubscriptionTestBase, SyncAsioContextTest {
|
||||
Subscription sub{ctx, "test"};
|
||||
std::shared_ptr<web::ConnectionBase> const session = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
};
|
||||
|
||||
TEST_F(SubscriptionMockPrometheusTest, subscribe)
|
||||
{
|
||||
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"test\"}");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
sub.subscribe(session);
|
||||
ctx.run();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionMockPrometheusTest, unsubscribe)
|
||||
{
|
||||
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"test\"}");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
sub.subscribe(session);
|
||||
ctx.run();
|
||||
EXPECT_CALL(counter, add(-1));
|
||||
sub.unsubscribe(session);
|
||||
ctx.restart();
|
||||
ctx.run();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionMockPrometheusTest, publish)
|
||||
{
|
||||
auto deadSession = std::make_shared<MockDeadSession>(tagDecoratorFactory);
|
||||
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"test\"}");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
sub.subscribe(deadSession);
|
||||
ctx.run();
|
||||
EXPECT_CALL(counter, add(-1));
|
||||
sub.publish(std::make_shared<std::string>("message"));
|
||||
sub.publish(std::make_shared<std::string>("message")); // Dead session is detected only after failed send
|
||||
ctx.restart();
|
||||
ctx.run();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionMockPrometheusTest, count)
|
||||
{
|
||||
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"test\"}");
|
||||
EXPECT_CALL(counter, value());
|
||||
sub.count();
|
||||
}
|
||||
|
||||
struct SubscriptionMapTest : SubscriptionTest {
|
||||
SubscriptionMap<std::string> subMap{ctx, "test"};
|
||||
};
|
||||
|
||||
TEST_F(SubscriptionMapTest, SubscriptionMapCount)
|
||||
{
|
||||
std::shared_ptr<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
std::shared_ptr<web::ConnectionBase> const session3 = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
SubscriptionMap<std::string> subMap(ctx);
|
||||
subMap.subscribe(session1, "topic1");
|
||||
subMap.subscribe(session2, "topic1");
|
||||
subMap.subscribe(session3, "topic2");
|
||||
@@ -160,7 +209,6 @@ TEST_F(SubscriptionMapTest, SubscriptionMapPublish)
|
||||
{
|
||||
std::shared_ptr<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
SubscriptionMap<std::string> subMap(ctx);
|
||||
const std::string topic1 = "topic1";
|
||||
const std::string topic2 = "topic2";
|
||||
const std::string topic1Message = "topic1Message";
|
||||
@@ -186,7 +234,6 @@ TEST_F(SubscriptionMapTest, SubscriptionMapDeadRemoveSubscriber)
|
||||
{
|
||||
std::shared_ptr<web::ConnectionBase> const session1(new MockDeadSession(tagDecoratorFactory));
|
||||
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
SubscriptionMap<std::string> subMap(ctx);
|
||||
const std::string topic1 = "topic1";
|
||||
const std::string topic2 = "topic2";
|
||||
const std::string topic1Message = "topic1Message";
|
||||
@@ -211,3 +258,51 @@ TEST_F(SubscriptionMapTest, SubscriptionMapDeadRemoveSubscriber)
|
||||
ctx.run();
|
||||
EXPECT_EQ(subMap.count(), 1);
|
||||
}
|
||||
|
||||
struct SubscriptionMapMockPrometheusTest : SubscriptionMockPrometheusTest {
|
||||
SubscriptionMap<std::string> subMap{ctx, "test"};
|
||||
std::shared_ptr<web::ConnectionBase> const session = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
};
|
||||
|
||||
TEST_F(SubscriptionMapMockPrometheusTest, subscribe)
|
||||
{
|
||||
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{collection=\"test\"}");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
subMap.subscribe(session, "topic");
|
||||
ctx.run();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionMapMockPrometheusTest, unsubscribe)
|
||||
{
|
||||
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{collection=\"test\"}");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
subMap.subscribe(session, "topic");
|
||||
ctx.run();
|
||||
EXPECT_CALL(counter, add(-1));
|
||||
subMap.unsubscribe(session, "topic");
|
||||
ctx.restart();
|
||||
ctx.run();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionMapMockPrometheusTest, publish)
|
||||
{
|
||||
auto deadSession = std::make_shared<MockDeadSession>(tagDecoratorFactory);
|
||||
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{collection=\"test\"}");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
subMap.subscribe(deadSession, "topic");
|
||||
ctx.run();
|
||||
EXPECT_CALL(counter, add(-1));
|
||||
subMap.publish(std::make_shared<std::string>("message"), "topic");
|
||||
subMap.publish(
|
||||
std::make_shared<std::string>("message"), "topic"
|
||||
); // Dead session is detected only after failed send
|
||||
ctx.restart();
|
||||
ctx.run();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionMapMockPrometheusTest, count)
|
||||
{
|
||||
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{collection=\"test\"}");
|
||||
EXPECT_CALL(counter, value());
|
||||
subMap.count();
|
||||
}
|
||||
|
||||
@@ -18,15 +18,16 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <data/BackendCounters.h>
|
||||
#include <util/MockPrometheus.h>
|
||||
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace data;
|
||||
using namespace util::prometheus;
|
||||
|
||||
class BackendCountersTest : public ::testing::Test {
|
||||
protected:
|
||||
struct BackendCountersTest : WithPrometheus {
|
||||
static boost::json::object
|
||||
emptyReport()
|
||||
{
|
||||
@@ -45,20 +46,21 @@ protected:
|
||||
})")
|
||||
.as_object();
|
||||
}
|
||||
|
||||
BackendCounters::PtrType const counters = BackendCounters::make();
|
||||
};
|
||||
|
||||
TEST_F(BackendCountersTest, EmptyByDefault)
|
||||
{
|
||||
auto const counters = BackendCounters::make();
|
||||
EXPECT_EQ(counters->report(), emptyReport());
|
||||
}
|
||||
|
||||
TEST_F(BackendCountersTest, RegisterTooBusy)
|
||||
{
|
||||
auto const counters = BackendCounters::make();
|
||||
counters->registerTooBusy();
|
||||
counters->registerTooBusy();
|
||||
counters->registerTooBusy();
|
||||
|
||||
auto expectedReport = emptyReport();
|
||||
expectedReport["too_busy"] = 3;
|
||||
EXPECT_EQ(counters->report(), expectedReport);
|
||||
@@ -66,9 +68,9 @@ TEST_F(BackendCountersTest, RegisterTooBusy)
|
||||
|
||||
TEST_F(BackendCountersTest, RegisterWriteSync)
|
||||
{
|
||||
auto const counters = BackendCounters::make();
|
||||
counters->registerWriteSync();
|
||||
counters->registerWriteSync();
|
||||
|
||||
auto expectedReport = emptyReport();
|
||||
expectedReport["write_sync"] = 2;
|
||||
EXPECT_EQ(counters->report(), expectedReport);
|
||||
@@ -76,10 +78,10 @@ TEST_F(BackendCountersTest, RegisterWriteSync)
|
||||
|
||||
TEST_F(BackendCountersTest, RegisterWriteSyncRetry)
|
||||
{
|
||||
auto const counters = BackendCounters::make();
|
||||
counters->registerWriteSyncRetry();
|
||||
counters->registerWriteSyncRetry();
|
||||
counters->registerWriteSyncRetry();
|
||||
|
||||
auto expectedReport = emptyReport();
|
||||
expectedReport["write_sync_retry"] = 3;
|
||||
EXPECT_EQ(counters->report(), expectedReport);
|
||||
@@ -87,9 +89,9 @@ TEST_F(BackendCountersTest, RegisterWriteSyncRetry)
|
||||
|
||||
TEST_F(BackendCountersTest, RegisterWriteStarted)
|
||||
{
|
||||
auto const counters = BackendCounters::make();
|
||||
counters->registerWriteStarted();
|
||||
counters->registerWriteStarted();
|
||||
|
||||
auto expectedReport = emptyReport();
|
||||
expectedReport["write_async_pending"] = 2;
|
||||
EXPECT_EQ(counters->report(), expectedReport);
|
||||
@@ -97,12 +99,12 @@ TEST_F(BackendCountersTest, RegisterWriteStarted)
|
||||
|
||||
TEST_F(BackendCountersTest, RegisterWriteFinished)
|
||||
{
|
||||
auto const counters = BackendCounters::make();
|
||||
counters->registerWriteStarted();
|
||||
counters->registerWriteStarted();
|
||||
counters->registerWriteStarted();
|
||||
counters->registerWriteFinished();
|
||||
counters->registerWriteFinished();
|
||||
|
||||
auto expectedReport = emptyReport();
|
||||
expectedReport["write_async_pending"] = 1;
|
||||
expectedReport["write_async_completed"] = 2;
|
||||
@@ -111,9 +113,9 @@ TEST_F(BackendCountersTest, RegisterWriteFinished)
|
||||
|
||||
TEST_F(BackendCountersTest, RegisterWriteRetry)
|
||||
{
|
||||
auto const counters = BackendCounters::make();
|
||||
counters->registerWriteRetry();
|
||||
counters->registerWriteRetry();
|
||||
|
||||
auto expectedReport = emptyReport();
|
||||
expectedReport["write_async_retry"] = 2;
|
||||
EXPECT_EQ(counters->report(), expectedReport);
|
||||
@@ -121,9 +123,9 @@ TEST_F(BackendCountersTest, RegisterWriteRetry)
|
||||
|
||||
TEST_F(BackendCountersTest, RegisterReadStarted)
|
||||
{
|
||||
auto const counters = BackendCounters::make();
|
||||
counters->registerReadStarted();
|
||||
counters->registerReadStarted();
|
||||
|
||||
auto expectedReport = emptyReport();
|
||||
expectedReport["read_async_pending"] = 2;
|
||||
EXPECT_EQ(counters->report(), expectedReport);
|
||||
@@ -131,12 +133,12 @@ TEST_F(BackendCountersTest, RegisterReadStarted)
|
||||
|
||||
TEST_F(BackendCountersTest, RegisterReadFinished)
|
||||
{
|
||||
auto const counters = BackendCounters::make();
|
||||
counters->registerReadStarted();
|
||||
counters->registerReadStarted();
|
||||
counters->registerReadStarted();
|
||||
counters->registerReadFinished();
|
||||
counters->registerReadFinished();
|
||||
|
||||
auto expectedReport = emptyReport();
|
||||
expectedReport["read_async_pending"] = 1;
|
||||
expectedReport["read_async_completed"] = 2;
|
||||
@@ -147,9 +149,10 @@ TEST_F(BackendCountersTest, RegisterReadStartedFinishedWithCounters)
|
||||
{
|
||||
static constexpr auto OPERATIONS_STARTED = 7u;
|
||||
static constexpr auto OPERATIONS_COMPLETED = 4u;
|
||||
auto const counters = BackendCounters::make();
|
||||
|
||||
counters->registerReadStarted(OPERATIONS_STARTED);
|
||||
counters->registerReadFinished(OPERATIONS_COMPLETED);
|
||||
|
||||
auto expectedReport = emptyReport();
|
||||
expectedReport["read_async_pending"] = OPERATIONS_STARTED - OPERATIONS_COMPLETED;
|
||||
expectedReport["read_async_completed"] = OPERATIONS_COMPLETED;
|
||||
@@ -171,13 +174,104 @@ TEST_F(BackendCountersTest, RegisterReadError)
|
||||
static constexpr auto OPERATIONS_STARTED = 7u;
|
||||
static constexpr auto OPERATIONS_ERROR = 2u;
|
||||
static constexpr auto OPERATIONS_COMPLETED = 1u;
|
||||
auto const counters = BackendCounters::make();
|
||||
|
||||
counters->registerReadStarted(OPERATIONS_STARTED);
|
||||
counters->registerReadError(OPERATIONS_ERROR);
|
||||
counters->registerReadFinished(OPERATIONS_COMPLETED);
|
||||
|
||||
auto expectedReport = emptyReport();
|
||||
expectedReport["read_async_pending"] = OPERATIONS_STARTED - OPERATIONS_COMPLETED - OPERATIONS_ERROR;
|
||||
expectedReport["read_async_completed"] = OPERATIONS_COMPLETED;
|
||||
expectedReport["read_async_error"] = OPERATIONS_ERROR;
|
||||
EXPECT_EQ(counters->report(), expectedReport);
|
||||
}
|
||||
|
||||
struct BackendCountersMockPrometheusTest : WithMockPrometheus {
|
||||
BackendCounters::PtrType const counters = BackendCounters::make();
|
||||
};
|
||||
|
||||
TEST_F(BackendCountersMockPrometheusTest, registerTooBusy)
|
||||
{
|
||||
auto& counter = makeMock<CounterInt>("backend_too_busy_total_number", "");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
counters->registerTooBusy();
|
||||
}
|
||||
|
||||
TEST_F(BackendCountersMockPrometheusTest, registerWriteSync)
|
||||
{
|
||||
auto& counter = makeMock<CounterInt>("backend_operations_total_number", "{operation=\"write_sync\"}");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
counters->registerWriteSync();
|
||||
}
|
||||
|
||||
TEST_F(BackendCountersMockPrometheusTest, registerWriteSyncRetry)
|
||||
{
|
||||
auto& counter = makeMock<CounterInt>("backend_operations_total_number", "{operation=\"write_sync_retry\"}");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
counters->registerWriteSyncRetry();
|
||||
}
|
||||
|
||||
TEST_F(BackendCountersMockPrometheusTest, registerWriteStarted)
|
||||
{
|
||||
auto& counter =
|
||||
makeMock<GaugeInt>("backend_operations_current_number", "{operation=\"write_async\",status=\"pending\"}");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
counters->registerWriteStarted();
|
||||
}
|
||||
|
||||
TEST_F(BackendCountersMockPrometheusTest, registerWriteFinished)
|
||||
{
|
||||
auto& pendingCounter =
|
||||
makeMock<GaugeInt>("backend_operations_current_number", "{operation=\"write_async\",status=\"pending\"}");
|
||||
auto& completedCounter =
|
||||
makeMock<CounterInt>("backend_operations_total_number", "{operation=\"write_async\",status=\"completed\"}");
|
||||
EXPECT_CALL(pendingCounter, add(-1));
|
||||
EXPECT_CALL(completedCounter, add(1));
|
||||
counters->registerWriteFinished();
|
||||
}
|
||||
|
||||
TEST_F(BackendCountersMockPrometheusTest, registerWriteRetry)
|
||||
{
|
||||
auto& counter =
|
||||
makeMock<CounterInt>("backend_operations_total_number", "{operation=\"write_async\",status=\"retry\"}");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
counters->registerWriteRetry();
|
||||
}
|
||||
|
||||
TEST_F(BackendCountersMockPrometheusTest, registerReadStarted)
|
||||
{
|
||||
auto& counter =
|
||||
makeMock<GaugeInt>("backend_operations_current_number", "{operation=\"read_async\",status=\"pending\"}");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
counters->registerReadStarted();
|
||||
}
|
||||
|
||||
TEST_F(BackendCountersMockPrometheusTest, registerReadFinished)
|
||||
{
|
||||
auto& pendingCounter =
|
||||
makeMock<GaugeInt>("backend_operations_current_number", "{operation=\"read_async\",status=\"pending\"}");
|
||||
auto& completedCounter =
|
||||
makeMock<CounterInt>("backend_operations_total_number", "{operation=\"read_async\",status=\"completed\"}");
|
||||
EXPECT_CALL(pendingCounter, add(-1));
|
||||
EXPECT_CALL(completedCounter, add(1));
|
||||
counters->registerReadFinished();
|
||||
}
|
||||
|
||||
TEST_F(BackendCountersMockPrometheusTest, registerReadRetry)
|
||||
{
|
||||
auto& counter =
|
||||
makeMock<CounterInt>("backend_operations_total_number", "{operation=\"read_async\",status=\"retry\"}");
|
||||
EXPECT_CALL(counter, add(1));
|
||||
counters->registerReadRetry();
|
||||
}
|
||||
|
||||
TEST_F(BackendCountersMockPrometheusTest, registerReadError)
|
||||
{
|
||||
auto& pendingCounter =
|
||||
makeMock<GaugeInt>("backend_operations_current_number", "{operation=\"read_async\",status=\"pending\"}");
|
||||
auto& errorCounter =
|
||||
makeMock<CounterInt>("backend_operations_total_number", "{operation=\"read_async\",status=\"error\"}");
|
||||
EXPECT_CALL(pendingCounter, add(-1));
|
||||
EXPECT_CALL(errorCounter, add(1));
|
||||
counters->registerReadError();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <util/Fixtures.h>
|
||||
#include <util/MockPrometheus.h>
|
||||
|
||||
#include <rpc/Counters.h>
|
||||
#include <rpc/JS.h>
|
||||
@@ -27,8 +28,11 @@
|
||||
|
||||
using namespace rpc;
|
||||
|
||||
class RPCCountersTest : public NoLoggerFixture {
|
||||
protected:
|
||||
using util::prometheus::CounterInt;
|
||||
using util::prometheus::WithMockPrometheus;
|
||||
using util::prometheus::WithPrometheus;
|
||||
|
||||
struct RPCCountersTest : WithPrometheus, NoLoggerFixture {
|
||||
WorkQueue queue{4u, 1024u}; // todo: mock instead
|
||||
Counters counters{queue};
|
||||
};
|
||||
@@ -95,3 +99,87 @@ TEST_F(RPCCountersTest, CheckThatCountersAddUp)
|
||||
|
||||
EXPECT_EQ(report.at("work_queue"), queue.report()); // Counters report includes queue report
|
||||
}
|
||||
|
||||
struct RPCCountersMockPrometheusTests : WithMockPrometheus {
|
||||
WorkQueue queue{4u, 1024u}; // todo: mock instead
|
||||
Counters counters{queue};
|
||||
};
|
||||
|
||||
TEST_F(RPCCountersMockPrometheusTests, rpcFailed)
|
||||
{
|
||||
auto& startedMock = makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"started\"}");
|
||||
auto& failedMock = makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"failed\"}");
|
||||
EXPECT_CALL(startedMock, add(1));
|
||||
EXPECT_CALL(failedMock, add(1));
|
||||
counters.rpcFailed("test");
|
||||
}
|
||||
|
||||
TEST_F(RPCCountersMockPrometheusTests, rpcErrored)
|
||||
{
|
||||
auto& startedMock = makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"started\"}");
|
||||
auto& erroredMock = makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"errored\"}");
|
||||
EXPECT_CALL(startedMock, add(1));
|
||||
EXPECT_CALL(erroredMock, add(1));
|
||||
counters.rpcErrored("test");
|
||||
}
|
||||
|
||||
TEST_F(RPCCountersMockPrometheusTests, rpcComplete)
|
||||
{
|
||||
auto& startedMock = makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"started\"}");
|
||||
auto& finishedMock = makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"finished\"}");
|
||||
auto& durationMock = makeMock<CounterInt>("rpc_method_duration_us", "{method=\"test\"}");
|
||||
EXPECT_CALL(startedMock, add(1));
|
||||
EXPECT_CALL(finishedMock, add(1));
|
||||
EXPECT_CALL(durationMock, add(123));
|
||||
counters.rpcComplete("test", std::chrono::microseconds(123));
|
||||
}
|
||||
|
||||
TEST_F(RPCCountersMockPrometheusTests, rpcForwarded)
|
||||
{
|
||||
auto& forwardedMock = makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"forwarded\"}");
|
||||
EXPECT_CALL(forwardedMock, add(1));
|
||||
counters.rpcForwarded("test");
|
||||
}
|
||||
|
||||
TEST_F(RPCCountersMockPrometheusTests, rpcFailedToForwarded)
|
||||
{
|
||||
auto& failedForwadMock =
|
||||
makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"failed_forward\"}");
|
||||
EXPECT_CALL(failedForwadMock, add(1));
|
||||
counters.rpcFailedToForward("test");
|
||||
}
|
||||
|
||||
TEST_F(RPCCountersMockPrometheusTests, onTooBusy)
|
||||
{
|
||||
auto& tooBusyMock = makeMock<CounterInt>("rpc_error_total_number", "{error_type=\"too_busy\"}");
|
||||
EXPECT_CALL(tooBusyMock, add(1));
|
||||
counters.onTooBusy();
|
||||
}
|
||||
|
||||
TEST_F(RPCCountersMockPrometheusTests, onNotReady)
|
||||
{
|
||||
auto& notReadyMock = makeMock<CounterInt>("rpc_error_total_number", "{error_type=\"not_ready\"}");
|
||||
EXPECT_CALL(notReadyMock, add(1));
|
||||
counters.onNotReady();
|
||||
}
|
||||
|
||||
TEST_F(RPCCountersMockPrometheusTests, onBadSyntax)
|
||||
{
|
||||
auto& badSyntaxMock = makeMock<CounterInt>("rpc_error_total_number", "{error_type=\"bad_syntax\"}");
|
||||
EXPECT_CALL(badSyntaxMock, add(1));
|
||||
counters.onBadSyntax();
|
||||
}
|
||||
|
||||
TEST_F(RPCCountersMockPrometheusTests, onUnknownCommand)
|
||||
{
|
||||
auto& unknownCommandMock = makeMock<CounterInt>("rpc_error_total_number", "{error_type=\"unknown_command\"}");
|
||||
EXPECT_CALL(unknownCommandMock, add(1));
|
||||
counters.onUnknownCommand();
|
||||
}
|
||||
|
||||
TEST_F(RPCCountersMockPrometheusTests, onInternalError)
|
||||
{
|
||||
auto& internalErrorMock = makeMock<CounterInt>("rpc_error_total_number", "{error_type=\"internal_error\"}");
|
||||
EXPECT_CALL(internalErrorMock, add(1));
|
||||
counters.onInternalError();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <util/Fixtures.h>
|
||||
#include <util/MockPrometheus.h>
|
||||
|
||||
#include <rpc/WorkQueue.h>
|
||||
|
||||
@@ -28,6 +29,7 @@
|
||||
|
||||
using namespace util;
|
||||
using namespace rpc;
|
||||
using namespace util::prometheus;
|
||||
|
||||
namespace {
|
||||
constexpr auto JSONConfig = R"JSON({
|
||||
@@ -36,36 +38,31 @@ constexpr auto JSONConfig = R"JSON({
|
||||
})JSON";
|
||||
} // namespace
|
||||
|
||||
class RPCWorkQueueTest : public NoLoggerFixture {
|
||||
protected:
|
||||
struct RPCWorkQueueTestBase : NoLoggerFixture {
|
||||
Config cfg = Config{boost::json::parse(JSONConfig)};
|
||||
WorkQueue queue = WorkQueue::make_WorkQueue(cfg);
|
||||
};
|
||||
|
||||
struct RPCWorkQueueTest : WithPrometheus, RPCWorkQueueTestBase {};
|
||||
|
||||
TEST_F(RPCWorkQueueTest, WhitelistedExecutionCountAddsUp)
|
||||
{
|
||||
WorkQueue queue = WorkQueue::make_WorkQueue(cfg);
|
||||
|
||||
auto constexpr static TOTAL = 512u;
|
||||
uint32_t executeCount = 0u;
|
||||
|
||||
std::binary_semaphore sem{0};
|
||||
std::mutex mtx;
|
||||
|
||||
for (auto i = 0u; i < TOTAL; ++i) {
|
||||
queue.postCoro(
|
||||
[&executeCount, &sem, &mtx](auto /* yield */) {
|
||||
[&executeCount, &mtx](auto /* yield */) {
|
||||
std::lock_guard const lk(mtx);
|
||||
if (++executeCount; executeCount == TOTAL)
|
||||
sem.release(); // 1) note we are still in user function
|
||||
++executeCount;
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
sem.acquire();
|
||||
|
||||
// 2) so we have to allow the size of queue to decrease by one asynchronously
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{1});
|
||||
queue.join();
|
||||
|
||||
auto const report = queue.report();
|
||||
|
||||
@@ -77,13 +74,10 @@ TEST_F(RPCWorkQueueTest, WhitelistedExecutionCountAddsUp)
|
||||
|
||||
TEST_F(RPCWorkQueueTest, NonWhitelistedPreventSchedulingAtQueueLimitExceeded)
|
||||
{
|
||||
auto queue = WorkQueue::make_WorkQueue(cfg);
|
||||
|
||||
auto constexpr static TOTAL = 3u;
|
||||
auto expectedCount = 2u;
|
||||
auto unblocked = false;
|
||||
|
||||
std::binary_semaphore sem{0};
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
|
||||
@@ -93,8 +87,7 @@ TEST_F(RPCWorkQueueTest, NonWhitelistedPreventSchedulingAtQueueLimitExceeded)
|
||||
std::unique_lock lk{mtx};
|
||||
cv.wait(lk, [&] { return unblocked; });
|
||||
|
||||
if (--expectedCount; expectedCount == 0)
|
||||
sem.release();
|
||||
--expectedCount;
|
||||
},
|
||||
false
|
||||
);
|
||||
@@ -110,6 +103,40 @@ TEST_F(RPCWorkQueueTest, NonWhitelistedPreventSchedulingAtQueueLimitExceeded)
|
||||
}
|
||||
}
|
||||
|
||||
sem.acquire();
|
||||
queue.join();
|
||||
EXPECT_TRUE(unblocked);
|
||||
}
|
||||
|
||||
struct RPCWorkQueueMockPrometheusTest : WithMockPrometheus, RPCWorkQueueTestBase {};
|
||||
|
||||
TEST_F(RPCWorkQueueMockPrometheusTest, postCoroCouhters)
|
||||
{
|
||||
auto& queuedMock = makeMock<CounterInt>("work_queue_queued_total_number", "");
|
||||
auto& durationMock = makeMock<CounterInt>("work_queue_cumulitive_tasks_duration_us", "");
|
||||
auto& curSizeMock = makeMock<GaugeInt>("work_queue_current_size", "");
|
||||
|
||||
std::mutex mtx;
|
||||
bool canContinue = false;
|
||||
std::condition_variable cv;
|
||||
|
||||
EXPECT_CALL(curSizeMock, value()).WillOnce(::testing::Return(0));
|
||||
EXPECT_CALL(curSizeMock, add(1));
|
||||
EXPECT_CALL(queuedMock, add(1));
|
||||
EXPECT_CALL(durationMock, add(::testing::Gt(0))).WillOnce([&](auto) {
|
||||
EXPECT_CALL(curSizeMock, add(-1));
|
||||
std::unique_lock const lk{mtx};
|
||||
canContinue = true;
|
||||
cv.notify_all();
|
||||
});
|
||||
|
||||
auto const res = queue.postCoro(
|
||||
[&](auto /* yield */) {
|
||||
std::unique_lock lk{mtx};
|
||||
cv.wait(lk, [&]() { return canContinue; });
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
queue.join();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <rpc/common/AnyHandler.h>
|
||||
#include <rpc/handlers/Subscribe.h>
|
||||
#include <util/Fixtures.h>
|
||||
#include <util/MockPrometheus.h>
|
||||
#include <util/MockWsBase.h>
|
||||
#include <util/TestObject.h>
|
||||
|
||||
@@ -43,7 +44,7 @@ constexpr static auto PAYS20XRPGETS10USDBOOKDIR = "7B1767D41DBCE79D9585CF9D0262A
|
||||
constexpr static auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
|
||||
constexpr static auto INDEX2 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
|
||||
|
||||
class RPCSubscribeHandlerTest : public HandlerBaseTest {
|
||||
class RPCSubscribeHandlerTest : public util::prometheus::WithPrometheus, public HandlerBaseTest {
|
||||
protected:
|
||||
void
|
||||
SetUp() override
|
||||
|
||||
181
unittests/util/MockPrometheus.h
Normal file
181
unittests/util/MockPrometheus.h
Normal file
@@ -0,0 +1,181 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <util/prometheus/Prometheus.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace util::prometheus {
|
||||
|
||||
template <detail::SomeNumberType NumberType>
|
||||
struct MockCounterImpl {
|
||||
using ValueType = NumberType;
|
||||
|
||||
MOCK_METHOD(void, add, (NumberType), ());
|
||||
MOCK_METHOD(void, set, (NumberType), ());
|
||||
MOCK_METHOD(NumberType, value, (), ());
|
||||
};
|
||||
|
||||
using MockCounterImplInt = MockCounterImpl<std::int64_t>;
|
||||
using MockCounterImplUint = MockCounterImpl<std::uint64_t>;
|
||||
using MockCounterImplDouble = MockCounterImpl<double>;
|
||||
|
||||
struct MockPrometheusImpl : PrometheusInterface {
|
||||
MockPrometheusImpl() : PrometheusInterface(true)
|
||||
{
|
||||
EXPECT_CALL(*this, counterInt)
|
||||
.WillRepeatedly([this](std::string name, Labels labels, std::optional<std::string>) -> CounterInt& {
|
||||
return getMetric<CounterInt>(std::move(name), std::move(labels));
|
||||
});
|
||||
EXPECT_CALL(*this, counterDouble)
|
||||
.WillRepeatedly([this](std::string name, Labels labels, std::optional<std::string>) -> CounterDouble& {
|
||||
return getMetric<CounterDouble>(std::move(name), std::move(labels));
|
||||
});
|
||||
EXPECT_CALL(*this, gaugeInt)
|
||||
.WillRepeatedly([this](std::string name, Labels labels, std::optional<std::string>) -> GaugeInt& {
|
||||
return getMetric<GaugeInt>(std::move(name), std::move(labels));
|
||||
});
|
||||
EXPECT_CALL(*this, gaugeDouble)
|
||||
.WillRepeatedly([this](std::string name, Labels labels, std::optional<std::string>) -> GaugeDouble& {
|
||||
return getMetric<GaugeDouble>(std::move(name), std::move(labels));
|
||||
});
|
||||
}
|
||||
|
||||
MOCK_METHOD(CounterInt&, counterInt, (std::string, Labels, std::optional<std::string>), (override));
|
||||
MOCK_METHOD(CounterDouble&, counterDouble, (std::string, Labels, std::optional<std::string>), (override));
|
||||
MOCK_METHOD(GaugeInt&, gaugeInt, (std::string, Labels, std::optional<std::string>), (override));
|
||||
MOCK_METHOD(GaugeDouble&, gaugeDouble, (std::string, Labels, std::optional<std::string>), (override));
|
||||
MOCK_METHOD(std::string, collectMetrics, (), (override));
|
||||
|
||||
template <typename MetricType>
|
||||
MetricType&
|
||||
getMetric(std::string name, Labels labels)
|
||||
{
|
||||
auto const labelsString = labels.serialize();
|
||||
auto const key = name + labels.serialize();
|
||||
auto it = metrics.find(key);
|
||||
if (it == metrics.end()) {
|
||||
return makeMetric<MetricType>(std::move(name), labels.serialize());
|
||||
}
|
||||
auto* basePtr = it->second.get();
|
||||
auto* metricPtr = dynamic_cast<MetricType*>(basePtr);
|
||||
if (metricPtr == nullptr)
|
||||
throw std::runtime_error("Wrong metric type");
|
||||
return *metricPtr;
|
||||
}
|
||||
|
||||
template <typename MetricType>
|
||||
MetricType&
|
||||
makeMetric(std::string name, std::string labelsString)
|
||||
{
|
||||
std::unique_ptr<MetricBase> metric;
|
||||
auto const key = name + labelsString;
|
||||
if constexpr (std::is_same_v<typename MetricType::ValueType, std::int64_t>) {
|
||||
auto& impl = counterIntImpls[key];
|
||||
metric = std::make_unique<MetricType>(name, labelsString, impl);
|
||||
} else if constexpr (std::is_same_v<typename MetricType::ValueType, std::uint64_t>) {
|
||||
auto& impl = counterUintImpls[key];
|
||||
metric = std::make_unique<MetricType>(name, labelsString, impl);
|
||||
} else {
|
||||
auto& impl = counterDoubleImpls[key];
|
||||
metric = std::make_unique<MetricType>(name, labelsString, impl);
|
||||
}
|
||||
auto* ptr = metrics.emplace(key, std::move(metric)).first->second.get();
|
||||
auto metricPtr = dynamic_cast<MetricType*>(ptr);
|
||||
if (metricPtr == nullptr)
|
||||
throw std::runtime_error("Wrong metric type");
|
||||
return *metricPtr;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::unique_ptr<MetricBase>> metrics;
|
||||
std::unordered_map<std::string, ::testing::StrictMock<MockCounterImplInt>> counterIntImpls;
|
||||
std::unordered_map<std::string, ::testing::StrictMock<MockCounterImplUint>> counterUintImpls;
|
||||
std::unordered_map<std::string, ::testing::StrictMock<MockCounterImplDouble>> counterDoubleImpls;
|
||||
};
|
||||
|
||||
/**
|
||||
* @note this class should be the first in the inheritance list
|
||||
*/
|
||||
struct WithMockPrometheus : virtual ::testing::Test {
|
||||
WithMockPrometheus()
|
||||
{
|
||||
PrometheusService::replaceInstance(std::make_unique<MockPrometheusImpl>());
|
||||
}
|
||||
|
||||
~WithMockPrometheus() override
|
||||
{
|
||||
if (HasFailure()) {
|
||||
std::cerr << "Registered metrics:\n";
|
||||
for (auto const& [key, metric] : mockPrometheus().metrics) {
|
||||
std::cerr << key << "\n";
|
||||
}
|
||||
std::cerr << "\n";
|
||||
}
|
||||
PrometheusService::init();
|
||||
}
|
||||
|
||||
static MockPrometheusImpl&
|
||||
mockPrometheus()
|
||||
{
|
||||
auto* ptr = dynamic_cast<MockPrometheusImpl*>(&PrometheusService::instance());
|
||||
if (ptr == nullptr)
|
||||
throw std::runtime_error("Wrong prometheus type");
|
||||
return *ptr;
|
||||
}
|
||||
|
||||
template <typename MetricType>
|
||||
static auto&
|
||||
makeMock(std::string name, std::string labelsString)
|
||||
{
|
||||
auto* mockPrometheusPtr = dynamic_cast<MockPrometheusImpl*>(&PrometheusService::instance());
|
||||
if (mockPrometheusPtr == nullptr)
|
||||
throw std::runtime_error("Wrong prometheus type");
|
||||
|
||||
std::string const key = name + labelsString;
|
||||
mockPrometheusPtr->makeMetric<MetricType>(std::move(name), std::move(labelsString));
|
||||
if constexpr (std::is_same_v<typename MetricType::ValueType, std::int64_t>) {
|
||||
return mockPrometheusPtr->counterIntImpls[key];
|
||||
} else if constexpr (std::is_same_v<typename MetricType::ValueType, std::uint64_t>) {
|
||||
return mockPrometheusPtr->counterUintImpls[key];
|
||||
} else if constexpr (std::is_same_v<typename MetricType::ValueType, double>) {
|
||||
return mockPrometheusPtr->counterDoubleImpls[key];
|
||||
}
|
||||
throw std::runtime_error("Wrong metric type");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @note this class should be the first in the inheritance list
|
||||
*/
|
||||
struct WithPrometheus : virtual ::testing::Test {
|
||||
WithPrometheus()
|
||||
{
|
||||
PrometheusService::init();
|
||||
}
|
||||
|
||||
~WithPrometheus() override
|
||||
{
|
||||
PrometheusService::init();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace util::prometheus
|
||||
@@ -47,6 +47,32 @@ struct HttpSyncClient {
|
||||
std::string const& body,
|
||||
std::vector<WebHeader> additionalHeaders = {}
|
||||
)
|
||||
{
|
||||
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::post);
|
||||
}
|
||||
|
||||
static std::string
|
||||
syncGet(
|
||||
std::string const& host,
|
||||
std::string const& port,
|
||||
std::string const& body,
|
||||
std::string const& target,
|
||||
std::vector<WebHeader> additionalHeaders = {}
|
||||
)
|
||||
{
|
||||
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::get, target);
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string
|
||||
syncRequest(
|
||||
std::string const& host,
|
||||
std::string const& port,
|
||||
std::string const& body,
|
||||
std::vector<WebHeader> additionalHeaders,
|
||||
http::verb method,
|
||||
std::string target = "/"
|
||||
)
|
||||
{
|
||||
boost::asio::io_context ioc;
|
||||
|
||||
@@ -56,14 +82,15 @@ struct HttpSyncClient {
|
||||
auto const results = resolver.resolve(host, port);
|
||||
stream.connect(results);
|
||||
|
||||
http::request<http::string_body> req{http::verb::post, "/", 10};
|
||||
http::request<http::string_body> req{method, "/", 10};
|
||||
req.set(http::field::host, host);
|
||||
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
|
||||
|
||||
for (auto& header : additionalHeaders) {
|
||||
req.set(header.name, std::move(header.value));
|
||||
req.set(header.name, header.value);
|
||||
}
|
||||
|
||||
req.target(target);
|
||||
req.body() = std::string(body);
|
||||
req.prepare_payload();
|
||||
http::write(stream, req);
|
||||
|
||||
157
unittests/util/prometheus/CounterTests.cpp
Normal file
157
unittests/util/prometheus/CounterTests.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <util/prometheus/Counter.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <thread>
|
||||
|
||||
using namespace util::prometheus;
|
||||
|
||||
struct AnyCounterTests : ::testing::Test {
|
||||
struct MockCounterImpl {
|
||||
using ValueType = std::uint64_t;
|
||||
MOCK_METHOD(void, add, (ValueType));
|
||||
MOCK_METHOD(void, set, (ValueType));
|
||||
MOCK_METHOD(ValueType, value, (), (const));
|
||||
};
|
||||
|
||||
::testing::StrictMock<MockCounterImpl> mockCounterImpl;
|
||||
std::string const name = "test_counter";
|
||||
std::string labelsString = R"({label1="value1",label2="value2"})";
|
||||
CounterInt counter{name, labelsString, static_cast<MockCounterImpl&>(mockCounterImpl)};
|
||||
};
|
||||
|
||||
TEST_F(AnyCounterTests, name)
|
||||
{
|
||||
EXPECT_EQ(counter.name(), name);
|
||||
}
|
||||
|
||||
TEST_F(AnyCounterTests, labelsString)
|
||||
{
|
||||
EXPECT_EQ(counter.labelsString(), labelsString);
|
||||
}
|
||||
|
||||
TEST_F(AnyCounterTests, serialize)
|
||||
{
|
||||
EXPECT_CALL(mockCounterImpl, value()).WillOnce(::testing::Return(42));
|
||||
std::string serialized;
|
||||
counter.serialize(serialized);
|
||||
EXPECT_EQ(serialized, R"(test_counter{label1="value1",label2="value2"} 42)");
|
||||
}
|
||||
|
||||
TEST_F(AnyCounterTests, operatorAdd)
|
||||
{
|
||||
EXPECT_CALL(mockCounterImpl, add(1));
|
||||
++counter;
|
||||
EXPECT_CALL(mockCounterImpl, add(42));
|
||||
counter += 42;
|
||||
}
|
||||
|
||||
TEST_F(AnyCounterTests, reset)
|
||||
{
|
||||
EXPECT_CALL(mockCounterImpl, set(0));
|
||||
counter.reset();
|
||||
}
|
||||
|
||||
TEST_F(AnyCounterTests, value)
|
||||
{
|
||||
EXPECT_CALL(mockCounterImpl, value()).WillOnce(::testing::Return(42));
|
||||
EXPECT_EQ(counter.value(), 42);
|
||||
}
|
||||
|
||||
struct CounterIntTests : ::testing::Test {
|
||||
CounterInt counter{"test_counter", R"(label1="value1",label2="value2")"};
|
||||
};
|
||||
|
||||
TEST_F(CounterIntTests, operatorAdd)
|
||||
{
|
||||
++counter;
|
||||
counter += 24;
|
||||
EXPECT_EQ(counter.value(), 25);
|
||||
}
|
||||
|
||||
TEST_F(CounterIntTests, reset)
|
||||
{
|
||||
++counter;
|
||||
EXPECT_EQ(counter.value(), 1);
|
||||
counter.reset();
|
||||
EXPECT_EQ(counter.value(), 0);
|
||||
}
|
||||
|
||||
TEST_F(CounterIntTests, multithreadAdd)
|
||||
{
|
||||
static auto constexpr numAdditions = 1000;
|
||||
static auto constexpr numNumberAdditions = 100;
|
||||
static auto constexpr numberToAdd = 11;
|
||||
std::thread thread1([&] {
|
||||
for (int i = 0; i < numAdditions; ++i) {
|
||||
++counter;
|
||||
}
|
||||
});
|
||||
std::thread thread2([&] {
|
||||
for (int i = 0; i < numNumberAdditions; ++i) {
|
||||
counter += numberToAdd;
|
||||
}
|
||||
});
|
||||
thread1.join();
|
||||
thread2.join();
|
||||
EXPECT_EQ(counter.value(), numAdditions + numNumberAdditions * numberToAdd);
|
||||
}
|
||||
|
||||
struct CounterDoubleTests : ::testing::Test {
|
||||
CounterDouble counter{"test_counter", R"(label1="value1",label2="value2")"};
|
||||
};
|
||||
|
||||
TEST_F(CounterDoubleTests, operatorAdd)
|
||||
{
|
||||
++counter;
|
||||
counter += 24.1234;
|
||||
EXPECT_NEAR(counter.value(), 25.1234, 1e-9);
|
||||
}
|
||||
|
||||
TEST_F(CounterDoubleTests, reset)
|
||||
{
|
||||
++counter;
|
||||
EXPECT_EQ(counter.value(), 1.);
|
||||
counter.reset();
|
||||
EXPECT_EQ(counter.value(), 0.);
|
||||
}
|
||||
|
||||
TEST_F(CounterDoubleTests, multithreadAdd)
|
||||
{
|
||||
static auto constexpr numAdditions = 1000;
|
||||
static auto constexpr numNumberAdditions = 100;
|
||||
static auto constexpr numberToAdd = 11.1234;
|
||||
std::thread thread1([&] {
|
||||
for (int i = 0; i < numAdditions; ++i) {
|
||||
++counter;
|
||||
}
|
||||
});
|
||||
std::thread thread2([&] {
|
||||
for (int i = 0; i < numNumberAdditions; ++i) {
|
||||
counter += numberToAdd;
|
||||
}
|
||||
});
|
||||
thread1.join();
|
||||
thread2.join();
|
||||
EXPECT_NEAR(counter.value(), numAdditions + numNumberAdditions * numberToAdd, 1e-9);
|
||||
}
|
||||
190
unittests/util/prometheus/GaugeTests.cpp
Normal file
190
unittests/util/prometheus/GaugeTests.cpp
Normal file
@@ -0,0 +1,190 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <util/prometheus/Gauge.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <thread>
|
||||
|
||||
using namespace util::prometheus;
|
||||
|
||||
struct AnyGaugeTests : ::testing::Test {
|
||||
struct MockGaugeImpl {
|
||||
using ValueType = std::int64_t;
|
||||
MOCK_METHOD(void, add, (ValueType));
|
||||
MOCK_METHOD(void, set, (ValueType));
|
||||
MOCK_METHOD(ValueType, value, (), (const));
|
||||
};
|
||||
|
||||
::testing::StrictMock<MockGaugeImpl> mockGaugeImpl;
|
||||
GaugeInt gauge{"test_gauge", R"(label1="value1",label2="value2")", static_cast<MockGaugeImpl&>(mockGaugeImpl)};
|
||||
};
|
||||
|
||||
TEST_F(AnyGaugeTests, operatorAdd)
|
||||
{
|
||||
EXPECT_CALL(mockGaugeImpl, add(1));
|
||||
++gauge;
|
||||
EXPECT_CALL(mockGaugeImpl, add(42));
|
||||
gauge += 42;
|
||||
}
|
||||
|
||||
TEST_F(AnyGaugeTests, operatorSubstract)
|
||||
{
|
||||
EXPECT_CALL(mockGaugeImpl, add(-1));
|
||||
--gauge;
|
||||
EXPECT_CALL(mockGaugeImpl, add(-42));
|
||||
gauge -= 42;
|
||||
}
|
||||
|
||||
TEST_F(AnyGaugeTests, set)
|
||||
{
|
||||
EXPECT_CALL(mockGaugeImpl, set(42));
|
||||
gauge.set(42);
|
||||
}
|
||||
|
||||
TEST_F(AnyGaugeTests, value)
|
||||
{
|
||||
EXPECT_CALL(mockGaugeImpl, value()).WillOnce(::testing::Return(42));
|
||||
EXPECT_EQ(gauge.value(), 42);
|
||||
}
|
||||
|
||||
struct GaugeIntTests : ::testing::Test {
|
||||
GaugeInt gauge{"test_Gauge", R"(label1="value1",label2="value2")"};
|
||||
};
|
||||
|
||||
TEST_F(GaugeIntTests, operatorAdd)
|
||||
{
|
||||
++gauge;
|
||||
gauge += 24;
|
||||
EXPECT_EQ(gauge.value(), 25);
|
||||
}
|
||||
|
||||
TEST_F(GaugeIntTests, operatorSubstract)
|
||||
{
|
||||
--gauge;
|
||||
EXPECT_EQ(gauge.value(), -1);
|
||||
}
|
||||
|
||||
TEST_F(GaugeIntTests, set)
|
||||
{
|
||||
gauge.set(21);
|
||||
EXPECT_EQ(gauge.value(), 21);
|
||||
}
|
||||
|
||||
TEST_F(GaugeIntTests, multithreadAddAndSubstract)
|
||||
{
|
||||
static constexpr auto numAdditions = 1000;
|
||||
static constexpr auto numNumberAdditions = 100;
|
||||
static constexpr auto numberToAdd = 11;
|
||||
static constexpr auto numSubstractions = 2000;
|
||||
static constexpr auto numNumberSubstractions = 300;
|
||||
static constexpr auto numberToSubstract = 300;
|
||||
std::thread thread1([&] {
|
||||
for (int i = 0; i < numAdditions; ++i) {
|
||||
++gauge;
|
||||
}
|
||||
});
|
||||
std::thread thread2([&] {
|
||||
for (int i = 0; i < numNumberAdditions; ++i) {
|
||||
gauge += numberToAdd;
|
||||
}
|
||||
});
|
||||
std::thread thread3([&] {
|
||||
for (int i = 0; i < numSubstractions; ++i) {
|
||||
--gauge;
|
||||
}
|
||||
});
|
||||
std::thread thread4([&] {
|
||||
for (int i = 0; i < numNumberSubstractions; ++i) {
|
||||
gauge -= numberToSubstract;
|
||||
}
|
||||
});
|
||||
thread1.join();
|
||||
thread2.join();
|
||||
thread3.join();
|
||||
thread4.join();
|
||||
EXPECT_EQ(
|
||||
gauge.value(),
|
||||
numAdditions + numNumberAdditions * numberToAdd - numSubstractions - numNumberSubstractions * numberToSubstract
|
||||
);
|
||||
}
|
||||
|
||||
struct GaugeDoubleTests : ::testing::Test {
|
||||
GaugeDouble gauge{"test_Gauge", R"(label1="value1",label2="value2")"};
|
||||
};
|
||||
|
||||
TEST_F(GaugeDoubleTests, operatorAdd)
|
||||
{
|
||||
++gauge;
|
||||
gauge += 24.1234;
|
||||
EXPECT_NEAR(gauge.value(), 25.1234, 1e-9);
|
||||
}
|
||||
|
||||
TEST_F(GaugeDoubleTests, operatorSubstract)
|
||||
{
|
||||
--gauge;
|
||||
EXPECT_EQ(gauge.value(), -1.0);
|
||||
}
|
||||
|
||||
TEST_F(GaugeDoubleTests, set)
|
||||
{
|
||||
gauge.set(21.1234);
|
||||
EXPECT_EQ(gauge.value(), 21.1234);
|
||||
}
|
||||
|
||||
TEST_F(GaugeDoubleTests, multithreadAddAndSubstract)
|
||||
{
|
||||
static constexpr auto numAdditions = 1000;
|
||||
static constexpr auto numNumberAdditions = 100;
|
||||
static constexpr auto numberToAdd = 11.1234;
|
||||
static constexpr auto numSubstractions = 2000;
|
||||
static constexpr auto numNumberSubstractions = 300;
|
||||
static constexpr auto numberToSubstract = 300.321;
|
||||
std::thread thread1([&] {
|
||||
for (int i = 0; i < numAdditions; ++i) {
|
||||
++gauge;
|
||||
}
|
||||
});
|
||||
std::thread thread2([&] {
|
||||
for (int i = 0; i < numNumberAdditions; ++i) {
|
||||
gauge += numberToAdd;
|
||||
}
|
||||
});
|
||||
std::thread thread3([&] {
|
||||
for (int i = 0; i < numSubstractions; ++i) {
|
||||
--gauge;
|
||||
}
|
||||
});
|
||||
std::thread thread4([&] {
|
||||
for (int i = 0; i < numNumberSubstractions; ++i) {
|
||||
gauge -= numberToSubstract;
|
||||
}
|
||||
});
|
||||
thread1.join();
|
||||
thread2.join();
|
||||
thread3.join();
|
||||
thread4.join();
|
||||
EXPECT_NEAR(
|
||||
gauge.value(),
|
||||
numAdditions + numNumberAdditions * numberToAdd - numSubstractions - numNumberSubstractions * numberToSubstract,
|
||||
1e-9
|
||||
);
|
||||
}
|
||||
213
unittests/util/prometheus/HttpTests.cpp
Normal file
213
unittests/util/prometheus/HttpTests.cpp
Normal file
@@ -0,0 +1,213 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <util/prometheus/Http.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace util::prometheus;
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
struct PrometheusCheckRequestTestsParams {
|
||||
std::string testName;
|
||||
http::verb method;
|
||||
std::string target;
|
||||
bool isAdmin;
|
||||
bool prometheusEnabled;
|
||||
bool expected;
|
||||
};
|
||||
|
||||
struct PrometheusCheckRequestTests : public ::testing::TestWithParam<PrometheusCheckRequestTestsParams> {
|
||||
struct NameGenerator {
|
||||
template <class ParamType>
|
||||
std::string
|
||||
operator()(testing::TestParamInfo<ParamType> const& info) const
|
||||
{
|
||||
auto bundle = static_cast<PrometheusCheckRequestTestsParams>(info.param);
|
||||
return bundle.testName;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
TEST_P(PrometheusCheckRequestTests, isPrometheusRequest)
|
||||
{
|
||||
PrometheusService::init(util::Config{boost::json::value{{"prometheus_enabled", GetParam().prometheusEnabled}}});
|
||||
boost::beast::http::request<boost::beast::http::string_body> req;
|
||||
req.method(GetParam().method);
|
||||
req.target(GetParam().target);
|
||||
EXPECT_EQ(handlePrometheusRequest(req, GetParam().isAdmin).has_value(), GetParam().expected);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
PrometheusHttpTests,
|
||||
PrometheusCheckRequestTests,
|
||||
::testing::ValuesIn({
|
||||
PrometheusCheckRequestTestsParams{
|
||||
.testName = "validRequest",
|
||||
.method = http::verb::get,
|
||||
.target = "/metrics",
|
||||
.isAdmin = true,
|
||||
.prometheusEnabled = true,
|
||||
.expected = true},
|
||||
PrometheusCheckRequestTestsParams{
|
||||
.testName = "validRequestPrometheusDisabled",
|
||||
.method = http::verb::get,
|
||||
.target = "/metrics",
|
||||
.isAdmin = true,
|
||||
.prometheusEnabled = false,
|
||||
.expected = true},
|
||||
PrometheusCheckRequestTestsParams{
|
||||
.testName = "notAdmin",
|
||||
.method = http::verb::get,
|
||||
.target = "/metrics",
|
||||
.isAdmin = false,
|
||||
.prometheusEnabled = true,
|
||||
.expected = true},
|
||||
PrometheusCheckRequestTestsParams{
|
||||
.testName = "wrongMethod",
|
||||
.method = http::verb::post,
|
||||
.target = "/metrics",
|
||||
.isAdmin = true,
|
||||
.prometheusEnabled = true,
|
||||
.expected = false},
|
||||
PrometheusCheckRequestTestsParams{
|
||||
.testName = "wrongTarget",
|
||||
.method = http::verb::get,
|
||||
.target = "/",
|
||||
.isAdmin = true,
|
||||
.prometheusEnabled = true,
|
||||
.expected = false},
|
||||
}),
|
||||
PrometheusCheckRequestTests::NameGenerator()
|
||||
);
|
||||
|
||||
struct PrometheusHandleRequestTests : ::testing::Test {
|
||||
PrometheusHandleRequestTests()
|
||||
{
|
||||
PrometheusService::init();
|
||||
}
|
||||
http::request<http::string_body> const req{http::verb::get, "/metrics", 11};
|
||||
};
|
||||
|
||||
TEST_F(PrometheusHandleRequestTests, emptyResponse)
|
||||
{
|
||||
auto response = handlePrometheusRequest(req, true);
|
||||
ASSERT_TRUE(response.has_value());
|
||||
EXPECT_EQ(response->result(), http::status::ok);
|
||||
EXPECT_EQ(response->operator[](http::field::content_type), "text/plain; version=0.0.4");
|
||||
EXPECT_EQ(response->body(), "");
|
||||
}
|
||||
|
||||
TEST_F(PrometheusHandleRequestTests, prometheusDisabled)
|
||||
{
|
||||
PrometheusService::init(util::Config(boost::json::value{{"prometheus_enabled", false}}));
|
||||
auto response = handlePrometheusRequest(req, true);
|
||||
ASSERT_TRUE(response.has_value());
|
||||
EXPECT_EQ(response->result(), http::status::forbidden);
|
||||
}
|
||||
|
||||
TEST_F(PrometheusHandleRequestTests, notAdmin)
|
||||
{
|
||||
auto response = handlePrometheusRequest(req, false);
|
||||
ASSERT_TRUE(response.has_value());
|
||||
EXPECT_EQ(response->result(), http::status::unauthorized);
|
||||
}
|
||||
|
||||
TEST_F(PrometheusHandleRequestTests, responseWithCounter)
|
||||
{
|
||||
auto const counterName = "test_counter";
|
||||
const Labels labels{{{"label1", "value1"}, Label{"label2", "value2"}}};
|
||||
auto const description = "test_description";
|
||||
|
||||
auto& counter = PrometheusService::counterInt(counterName, labels, description);
|
||||
++counter;
|
||||
counter += 3;
|
||||
|
||||
auto response = handlePrometheusRequest(req, true);
|
||||
ASSERT_TRUE(response.has_value());
|
||||
EXPECT_EQ(response->result(), http::status::ok);
|
||||
EXPECT_EQ(response->operator[](http::field::content_type), "text/plain; version=0.0.4");
|
||||
auto const expectedBody =
|
||||
fmt::format("# HELP {0} {1}\n# TYPE {0} counter\n{0}{2} 4\n\n", counterName, description, labels.serialize());
|
||||
EXPECT_EQ(response->body(), expectedBody);
|
||||
}
|
||||
|
||||
TEST_F(PrometheusHandleRequestTests, responseWithGauge)
|
||||
{
|
||||
auto const gaugeName = "test_gauge";
|
||||
const Labels labels{{{"label2", "value2"}, Label{"label3", "value3"}}};
|
||||
auto const description = "test_description_gauge";
|
||||
|
||||
auto& gauge = PrometheusService::gaugeInt(gaugeName, labels, description);
|
||||
++gauge;
|
||||
gauge -= 3;
|
||||
|
||||
auto response = handlePrometheusRequest(req, true);
|
||||
ASSERT_TRUE(response.has_value());
|
||||
EXPECT_EQ(response->result(), http::status::ok);
|
||||
EXPECT_EQ(response->operator[](http::field::content_type), "text/plain; version=0.0.4");
|
||||
auto const expectedBody =
|
||||
fmt::format("# HELP {0} {1}\n# TYPE {0} gauge\n{0}{2} -2\n\n", gaugeName, description, labels.serialize());
|
||||
EXPECT_EQ(response->body(), expectedBody);
|
||||
}
|
||||
|
||||
TEST_F(PrometheusHandleRequestTests, responseWithCounterAndGauge)
|
||||
{
|
||||
auto const counterName = "test_counter";
|
||||
const Labels counterLabels{{{"label1", "value1"}, Label{"label2", "value2"}}};
|
||||
auto const counterDescription = "test_description";
|
||||
|
||||
auto& counter = PrometheusService::counterInt(counterName, counterLabels, counterDescription);
|
||||
++counter;
|
||||
counter += 3;
|
||||
|
||||
auto const gaugeName = "test_gauge";
|
||||
const Labels gaugeLabels{{{"label2", "value2"}, Label{"label3", "value3"}}};
|
||||
auto const gaugeDescription = "test_description_gauge";
|
||||
|
||||
auto& gauge = PrometheusService::gaugeInt(gaugeName, gaugeLabels, gaugeDescription);
|
||||
++gauge;
|
||||
gauge -= 3;
|
||||
|
||||
auto response = handlePrometheusRequest(req, true);
|
||||
|
||||
EXPECT_EQ(response->result(), http::status::ok);
|
||||
EXPECT_EQ(response->operator[](http::field::content_type), "text/plain; version=0.0.4");
|
||||
auto const expectedBody = fmt::format(
|
||||
"# HELP {3} {4}\n# TYPE {3} gauge\n{3}{5} -2\n\n"
|
||||
"# HELP {0} {1}\n# TYPE {0} counter\n{0}{2} 4\n\n",
|
||||
counterName,
|
||||
counterDescription,
|
||||
counterLabels.serialize(),
|
||||
gaugeName,
|
||||
gaugeDescription,
|
||||
gaugeLabels.serialize()
|
||||
);
|
||||
auto const anotherExpectedBody = fmt::format(
|
||||
"# HELP {0} {1}\n# TYPE {0} counter\n{0}{2} 4\n\n"
|
||||
"# HELP {3} {4}\n# TYPE {3} gauge\n{3}{5} -2\n\n",
|
||||
counterName,
|
||||
counterDescription,
|
||||
counterLabels.serialize(),
|
||||
gaugeName,
|
||||
gaugeDescription,
|
||||
gaugeLabels.serialize()
|
||||
);
|
||||
EXPECT_TRUE(response->body() == expectedBody || response->body() == anotherExpectedBody);
|
||||
}
|
||||
54
unittests/util/prometheus/LabelTests.cpp
Normal file
54
unittests/util/prometheus/LabelTests.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <util/prometheus/Label.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace util::prometheus;
|
||||
|
||||
TEST(LabelTests, operatorLower)
|
||||
{
|
||||
EXPECT_LT(Label("aaa", "b"), Label("bbb", "a"));
|
||||
EXPECT_LT(Label("name", "a"), Label("name", "b"));
|
||||
}
|
||||
|
||||
TEST(LabelTests, operatorEquals)
|
||||
{
|
||||
EXPECT_EQ(Label("aaa", "b"), Label("aaa", "b"));
|
||||
EXPECT_NE(Label("aaa", "b"), Label("aaa", "c"));
|
||||
EXPECT_NE(Label("aaa", "b"), Label("bbb", "b"));
|
||||
}
|
||||
|
||||
TEST(LabelTests, serialize)
|
||||
{
|
||||
EXPECT_EQ(Label("name", "value").serialize(), R"(name="value")");
|
||||
EXPECT_EQ(Label("name", "value\n").serialize(), R"(name="value\n")");
|
||||
EXPECT_EQ(Label("name", "value\\").serialize(), R"(name="value\\")");
|
||||
EXPECT_EQ(Label("name", "value\"").serialize(), R"(name="value\"")");
|
||||
}
|
||||
|
||||
TEST(LabelsTest, serialize)
|
||||
{
|
||||
EXPECT_EQ(Labels().serialize(), "");
|
||||
EXPECT_EQ(Labels({Label("name", "value")}).serialize(), R"({name="value"})");
|
||||
EXPECT_EQ(
|
||||
Labels({Label("name", "value"), Label("name2", "value2")}).serialize(), R"({name="value",name2="value2"})"
|
||||
);
|
||||
}
|
||||
130
unittests/util/prometheus/MetricsTests.cpp
Normal file
130
unittests/util/prometheus/MetricsTests.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <util/prometheus/Counter.h>
|
||||
#include <util/prometheus/Gauge.h>
|
||||
#include <util/prometheus/Metrics.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace util::prometheus;
|
||||
|
||||
TEST(DefaultMetricBuilderTest, build)
|
||||
{
|
||||
std::string const name = "name";
|
||||
std::string const labelsString = "{label1=\"value1\"}";
|
||||
for (auto const type :
|
||||
{MetricType::COUNTER_INT, MetricType::COUNTER_DOUBLE, MetricType::GAUGE_INT, MetricType::GAUGE_DOUBLE}) {
|
||||
auto metric = MetricsFamily::defaultMetricBuilder(name, labelsString, type);
|
||||
switch (type) {
|
||||
case MetricType::COUNTER_INT:
|
||||
EXPECT_NE(dynamic_cast<CounterInt*>(metric.get()), nullptr);
|
||||
break;
|
||||
case MetricType::COUNTER_DOUBLE:
|
||||
EXPECT_NE(dynamic_cast<CounterDouble*>(metric.get()), nullptr);
|
||||
break;
|
||||
case MetricType::GAUGE_INT:
|
||||
EXPECT_NE(dynamic_cast<GaugeInt*>(metric.get()), nullptr);
|
||||
break;
|
||||
case MetricType::GAUGE_DOUBLE:
|
||||
EXPECT_NE(dynamic_cast<GaugeDouble*>(metric.get()), nullptr);
|
||||
break;
|
||||
default:
|
||||
EXPECT_EQ(metric, nullptr);
|
||||
}
|
||||
if (metric != nullptr) {
|
||||
EXPECT_EQ(metric->name(), name);
|
||||
EXPECT_EQ(metric->labelsString(), labelsString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MetricsFamilyTest : ::testing::Test {
|
||||
struct MetricMock : MetricBase {
|
||||
using MetricBase::MetricBase;
|
||||
MOCK_METHOD(void, serializeValue, (std::string&), (const));
|
||||
};
|
||||
using MetricStrictMock = ::testing::StrictMock<MetricMock>;
|
||||
|
||||
struct MetricBuilderImplMock {
|
||||
MOCK_METHOD(std::unique_ptr<MetricBase>, build, (std::string, std::string, MetricType));
|
||||
};
|
||||
|
||||
::testing::StrictMock<MetricBuilderImplMock> metricBuilderMock;
|
||||
MetricsFamily::MetricBuilder metricBuilder =
|
||||
[this](std::string metricName, std::string labels, MetricType metricType) {
|
||||
return metricBuilderMock.build(std::move(metricName), std::move(labels), metricType);
|
||||
};
|
||||
|
||||
std::string const name{"name"};
|
||||
std::string const description{"description"};
|
||||
MetricType const type{MetricType::COUNTER_INT};
|
||||
MetricsFamily metricsFamily{name, description, type, metricBuilder};
|
||||
};
|
||||
|
||||
TEST_F(MetricsFamilyTest, getters)
|
||||
{
|
||||
EXPECT_EQ(metricsFamily.name(), name);
|
||||
EXPECT_EQ(metricsFamily.type(), type);
|
||||
}
|
||||
|
||||
TEST_F(MetricsFamilyTest, getMetric)
|
||||
{
|
||||
Labels const labels{{{"label1", "value1"}}};
|
||||
std::string const labelsString = labels.serialize();
|
||||
|
||||
EXPECT_CALL(metricBuilderMock, build(name, labelsString, type))
|
||||
.WillOnce(::testing::Return(std::make_unique<MetricStrictMock>(name, labelsString)));
|
||||
|
||||
auto& metric = metricsFamily.getMetric(labels);
|
||||
EXPECT_EQ(metric.name(), name);
|
||||
EXPECT_EQ(metric.labelsString(), labelsString);
|
||||
|
||||
auto* metricMock = dynamic_cast<MetricStrictMock*>(&metric);
|
||||
ASSERT_NE(metricMock, nullptr);
|
||||
EXPECT_EQ(&metricsFamily.getMetric(labels), &metric);
|
||||
|
||||
Labels const labels2{{{"label1", "value2"}}};
|
||||
std::string const labels2String = labels2.serialize();
|
||||
|
||||
EXPECT_CALL(metricBuilderMock, build(name, labels2String, type))
|
||||
.WillOnce(::testing::Return(std::make_unique<MetricStrictMock>(name, labels2String)));
|
||||
|
||||
auto& metric2 = metricsFamily.getMetric(labels2);
|
||||
EXPECT_EQ(metric2.name(), name);
|
||||
EXPECT_EQ(metric2.labelsString(), labels2String);
|
||||
|
||||
auto* metric2Mock = dynamic_cast<MetricStrictMock*>(&metric2);
|
||||
ASSERT_NE(metric2Mock, nullptr);
|
||||
EXPECT_EQ(&metricsFamily.getMetric(labels2), &metric2);
|
||||
EXPECT_NE(&metric, &metric2);
|
||||
|
||||
EXPECT_CALL(*metricMock, serializeValue(::testing::_)).WillOnce([](std::string& s) { s += "metric"; });
|
||||
EXPECT_CALL(*metric2Mock, serializeValue(::testing::_)).WillOnce([](std::string& s) { s += "metric2"; });
|
||||
|
||||
std::string serialized;
|
||||
metricsFamily.serialize(serialized);
|
||||
|
||||
auto const expected =
|
||||
fmt::format("# HELP {0} {1}\n# TYPE {0} {2}\nmetric\nmetric2\n\n", name, description, toString(type));
|
||||
auto const anotherExpected =
|
||||
fmt::format("# HELP {0} {1}\n# TYPE {0} {2}\nmetric2\nmetric\n\n", name, description, toString(type));
|
||||
EXPECT_TRUE(serialized == expected || serialized == anotherExpected);
|
||||
}
|
||||
@@ -52,7 +52,7 @@ protected:
|
||||
makeRequest(std::string const& password, http::field const field = http::field::authorization)
|
||||
{
|
||||
http::request<http::string_body> request = {};
|
||||
request.set(field, password);
|
||||
request.set(field, "Password " + password);
|
||||
return request;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <util/Fixtures.h>
|
||||
#include <util/MockPrometheus.h>
|
||||
#include <util/TestHttpSyncClient.h>
|
||||
#include <web/Server.h>
|
||||
|
||||
@@ -399,16 +400,6 @@ static auto constexpr JSONServerConfigWithAdminPassword = R"JSON(
|
||||
}
|
||||
)JSON";
|
||||
|
||||
static auto constexpr JSONServerConfigWithAdminPasswordWithFalseLocalAdmin = R"JSON(
|
||||
{
|
||||
"server":{
|
||||
"ip": "0.0.0.0",
|
||||
"port": 8888,
|
||||
"admin_password": "secret"
|
||||
}
|
||||
}
|
||||
)JSON";
|
||||
|
||||
static auto constexpr JSONServerConfigWithLocalAdmin = R"JSON(
|
||||
{
|
||||
"server":{
|
||||
@@ -507,18 +498,31 @@ INSTANTIATE_TEST_CASE_P(
|
||||
WebServerAdminTestParams{
|
||||
.config = JSONServerConfigWithAdminPassword,
|
||||
.headers = {WebHeader(http::field::authorization, SecertSha256)},
|
||||
.expectedResponse = "admin"},
|
||||
.expectedResponse = "user"},
|
||||
WebServerAdminTestParams{
|
||||
.config = JSONServerConfigWithAdminPasswordWithFalseLocalAdmin,
|
||||
.headers = {WebHeader(http::field::authorization, SecertSha256)},
|
||||
.config = JSONServerConfigWithAdminPassword,
|
||||
.headers = {WebHeader(
|
||||
http::field::authorization,
|
||||
fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecertSha256)
|
||||
)},
|
||||
.expectedResponse = "admin"},
|
||||
WebServerAdminTestParams{
|
||||
.config = JSONServerConfigWithBothAdminPasswordAndLocalAdminFalse,
|
||||
.headers = {WebHeader(http::field::authorization, SecertSha256)},
|
||||
.expectedResponse = "user"},
|
||||
WebServerAdminTestParams{
|
||||
.config = JSONServerConfigWithBothAdminPasswordAndLocalAdminFalse,
|
||||
.headers = {WebHeader(
|
||||
http::field::authorization,
|
||||
fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecertSha256)
|
||||
)},
|
||||
.expectedResponse = "admin"},
|
||||
WebServerAdminTestParams{
|
||||
.config = JSONServerConfigWithAdminPassword,
|
||||
.headers = {WebHeader(http::field::authentication_info, SecertSha256)},
|
||||
.headers = {WebHeader(
|
||||
http::field::authentication_info,
|
||||
fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecertSha256)
|
||||
)},
|
||||
.expectedResponse = "user"},
|
||||
WebServerAdminTestParams{.config = JSONServerConfigWithLocalAdmin, .headers = {}, .expectedResponse = "admin"},
|
||||
WebServerAdminTestParams{
|
||||
@@ -563,3 +567,64 @@ TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminFalse)
|
||||
Config const serverConfig{boost::json::parse(JSONServerConfigWithNoAdminPasswordAndLocalAdminFalse)};
|
||||
EXPECT_THROW(web::make_HttpServer(serverConfig, ctx, std::nullopt, dosGuardOverload, e), std::logic_error);
|
||||
}
|
||||
|
||||
struct WebServerPrometheusTest : util::prometheus::WithPrometheus, WebServerTest {};
|
||||
|
||||
TEST_F(WebServerPrometheusTest, rejectedWithoutAdminPassword)
|
||||
{
|
||||
auto e = std::make_shared<EchoExecutor>();
|
||||
Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword)};
|
||||
auto server = makeServerSync(serverConfig, ctx, std::nullopt, dosGuard, e);
|
||||
auto const res = HttpSyncClient::syncGet("localhost", "8888", "", "/metrics");
|
||||
EXPECT_EQ(res, "Only admin is allowed to collect metrics");
|
||||
}
|
||||
|
||||
TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled)
|
||||
{
|
||||
static auto constexpr JSONServerConfigWithDisabledPrometheus = R"JSON(
|
||||
{
|
||||
"server":{
|
||||
"ip": "0.0.0.0",
|
||||
"port": 8888,
|
||||
"admin_password": "secret"
|
||||
},
|
||||
"prometheus_enabled": false
|
||||
}
|
||||
)JSON";
|
||||
|
||||
auto e = std::make_shared<EchoExecutor>();
|
||||
Config const serverConfig{boost::json::parse(JSONServerConfigWithDisabledPrometheus)};
|
||||
PrometheusService::init(serverConfig);
|
||||
auto server = makeServerSync(serverConfig, ctx, std::nullopt, dosGuard, e);
|
||||
auto const res = HttpSyncClient::syncGet(
|
||||
"localhost",
|
||||
"8888",
|
||||
"",
|
||||
"/metrics",
|
||||
{WebHeader(
|
||||
http::field::authorization,
|
||||
fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecertSha256)
|
||||
)}
|
||||
);
|
||||
EXPECT_EQ(res, "Prometheus is disabled in clio config");
|
||||
}
|
||||
|
||||
TEST_F(WebServerPrometheusTest, validResponse)
|
||||
{
|
||||
auto& testCounter = PrometheusService::counterInt("test_counter", util::prometheus::Labels());
|
||||
++testCounter;
|
||||
auto e = std::make_shared<EchoExecutor>();
|
||||
Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword)};
|
||||
auto server = makeServerSync(serverConfig, ctx, std::nullopt, dosGuard, e);
|
||||
auto const res = HttpSyncClient::syncGet(
|
||||
"localhost",
|
||||
"8888",
|
||||
"",
|
||||
"/metrics",
|
||||
{WebHeader(
|
||||
http::field::authorization,
|
||||
fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecertSha256)
|
||||
)}
|
||||
);
|
||||
EXPECT_EQ(res, "# TYPE test_counter counter\ntest_counter 1\n\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user