Add prometheus support (#950)

Fixes #888
This commit is contained in:
Sergey Kuznetsov
2023-11-02 17:26:03 +00:00
committed by GitHub
parent 320ebaa5d2
commit a16b680a7a
50 changed files with 4322 additions and 178 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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%",

View 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.

View 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

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -0,0 +1,8 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus:9090
isDefault: true
access: proxy

View 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

View File

@@ -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

View File

@@ -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"};

View File

@@ -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

View File

@@ -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_;

View File

@@ -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_);

View File

@@ -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";

View File

@@ -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();

View File

@@ -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.

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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>

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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_;

View 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_;
};

View 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

View 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

View File

@@ -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>

View File

@@ -60,6 +60,8 @@ private:
std::string passwordSha256_;
public:
static constexpr std::string_view passwordPrefix = "Password ";
PasswordAdminVerificationStrategy(std::string const& password);
/**

View File

@@ -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"));
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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

View 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

View File

@@ -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);

View 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);
}

View 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
);
}

View 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);
}

View 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"})"
);
}

View 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);
}

View File

@@ -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;
}
};

View File

@@ -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");
}