diff --git a/CMakeLists.txt b/CMakeLists.txt index 227ad9f7..b3d48a7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,7 +144,10 @@ target_sources (clio PRIVATE src/util/log/Logger.cpp src/util/prometheus/Http.cpp src/util/prometheus/Label.cpp - src/util/prometheus/Metrics.cpp + src/util/prometheus/MetricBase.cpp + src/util/prometheus/MetricBuilder.cpp + src/util/prometheus/MetricsFamily.cpp + src/util/prometheus/OStream.cpp src/util/prometheus/Prometheus.cpp src/util/Random.cpp src/util/Taggable.cpp @@ -178,9 +181,12 @@ if (tests) unittests/util/StringUtils.cpp unittests/util/prometheus/CounterTests.cpp unittests/util/prometheus/GaugeTests.cpp + unittests/util/prometheus/HistogramTests.cpp unittests/util/prometheus/HttpTests.cpp unittests/util/prometheus/LabelTests.cpp - unittests/util/prometheus/MetricsTests.cpp + unittests/util/prometheus/MetricBuilderTests.cpp + unittests/util/prometheus/MetricsFamilyTests.cpp + unittests/util/prometheus/OStreamTests.cpp # ETL unittests/etl/ExtractionDataPipeTests.cpp unittests/etl/ExtractorTests.cpp diff --git a/README.md b/README.md index c716b3e5..5cb76be0 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ Exactly equal password gains admin rights for the request or a websocket connect ## 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. +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). diff --git a/example-config.json b/example-config.json index 504d1ba5..030c25fc 100644 --- a/example-config.json +++ b/example-config.json @@ -101,7 +101,10 @@ "log_level": "trace" } ], - "prometheus_enabled": true, + "prometheus": { + "enabled": true, + "compress_reply": true, + }, "log_level": "info", // Log format (this is the default format) "log_format": "%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%", diff --git a/examples/infrastructure/grafana/clio_dashboard.json b/examples/infrastructure/grafana/clio_dashboard.json index 40e13098..08a7bead 100644 --- a/examples/infrastructure/grafana/clio_dashboard.json +++ b/examples/infrastructure/grafana/clio_dashboard.json @@ -68,7 +68,7 @@ }, "gridPos": { "h": 8, - "w": 5, + "w": 3, "x": 0, "y": 0 }, @@ -105,102 +105,6 @@ "title": "Service state", "type": "stat" }, - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 7, - "x": 5, - "y": 0 - }, - "id": 7, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "code", - "expr": "scrape_duration_seconds{job=\"clio\"}", - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "Prometheus Request Processing Time", - "type": "timeseries" - }, { "datasource": { "type": "prometheus", @@ -262,8 +166,8 @@ }, "gridPos": { "h": 8, - "w": 12, - "x": 12, + "w": 9, + "x": 3, "y": 0 }, "id": 2, @@ -296,102 +200,6 @@ "title": "Work Queue Size", "type": "timeseries" }, - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "µs" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 8 - }, - "id": 10, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "code", - "expr": "rpc_method_duration_us{job=\"clio\"}", - "instant": false, - "legendFormat": "{{method}}", - "range": true, - "refId": "A" - } - ], - "title": "RPC Method Call Duration", - "type": "timeseries" - }, { "datasource": { "type": "prometheus", @@ -455,7 +263,7 @@ "h": 8, "w": 12, "x": 12, - "y": 8 + "y": 0 }, "id": 9, "options": { @@ -550,9 +358,9 @@ "h": 8, "w": 12, "x": 0, - "y": 16 + "y": 8 }, - "id": 8, + "id": 11, "options": { "legend": { "calcs": [], @@ -572,14 +380,206 @@ "uid": "PBFA97CFB590B2093" }, "editorMode": "code", - "expr": "rate(rpc_error_total_number{job=\"clio\"}[$__rate_interval])", + "expr": "subscriptions_current_number{job=\"clio\"}", "instant": false, - "legendFormat": "{{error_type}}", + "legendFormat": "{{collection}}{{stream}}", "range": true, "refId": "A" } ], - "title": "RPC Error Rate", + "title": "Subscriptions Number", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(increase(ledger_cache_counter_total_number{job=\"clio\",type=\"cache_hit\"}[1m])) / sum(increase(ledger_cache_counter_total_number{job=\"clio\",type=\"request\"}[1m]))", + "hide": false, + "instant": false, + "legendFormat": "ledger cache hit rate", + "range": true, + "refId": "A" + } + ], + "title": "Ledger Cache Hit Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "µs" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "rpc_method_duration_us{job=\"clio\"}", + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A" + } + ], + "title": "RPC Method Call Duration", "type": "timeseries" }, { @@ -647,13 +647,13 @@ "x": 12, "y": 16 }, - "id": 6, + "id": 8, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true }, "tooltip": { "mode": "single", @@ -667,15 +667,14 @@ "uid": "PBFA97CFB590B2093" }, "editorMode": "code", - "expr": "sum(increase(ledger_cache_counter_total_number{job=\"clio\",type=\"cache_hit\"}[1m])) / sum(increase(ledger_cache_counter_total_number{job=\"clio\",type=\"request\"}[1m]))", - "hide": false, + "expr": "rate(rpc_error_total_number{job=\"clio\"}[$__rate_interval])", "instant": false, - "legendFormat": "ledger cache hit rate", + "legendFormat": "{{error_type}}", "range": true, "refId": "A" } ], - "title": "Ledger Cache Hit Rate", + "title": "RPC Error Rate", "type": "timeseries" }, { @@ -791,7 +790,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 0, + "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, @@ -800,6 +799,9 @@ }, "insertNulls": false, "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, "lineWidth": 1, "pointSize": 5, "scaleDistribution": { @@ -828,7 +830,8 @@ "value": 80 } ] - } + }, + "unit": "ms" }, "overrides": [] }, @@ -838,7 +841,7 @@ "x": 12, "y": 24 }, - "id": 11, + "id": 12, "options": { "legend": { "calcs": [], @@ -851,6 +854,7 @@ "sort": "none" } }, + "pluginVersion": "10.2.0", "targets": [ { "datasource": { @@ -858,14 +862,52 @@ "uid": "PBFA97CFB590B2093" }, "editorMode": "code", - "expr": "subscriptions_current_number{job=\"clio\"}", + "expr": "histogram_quantile(0.50, sum(rate(backend_duration_milliseconds_histogram_bucket{job=\"clio\"}[$__interval])) by (le, operation))", + "hide": false, "instant": false, - "legendFormat": "{{collection}}{{stream}}", + "legendFormat": "{{operation}} 0.5 percentile", "range": true, "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.75, sum(rate(backend_duration_milliseconds_histogram_bucket{job=\"clio\"}[$__interval])) by (le, operation))", + "hide": false, + "instant": false, + "legendFormat": "{{operation}} 0.75 percentile", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(backend_duration_milliseconds_histogram_bucket{job=\"clio\"}[$__interval])) by (le, operation))", + "hide": false, + "instant": false, + "legendFormat": "{{operation}} 0.95 percentile", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "", + "hide": false, + "instant": false, + "range": true, + "refId": "D" } ], - "title": "Subscriptions Number", + "title": "DB operation duration", "type": "timeseries" }, { @@ -1081,6 +1123,102 @@ ], "title": "DB Pending operations", "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 10, + "x": 0, + "y": 40 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "scrape_duration_seconds{job=\"clio\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Prometheus Request Processing Time", + "type": "timeseries" } ], "refresh": "5s", @@ -1097,6 +1235,6 @@ "timezone": "", "title": "Clio", "uid": "aeaae84e-c194-47b2-ad65-86e45eebb815", - "version": 1, + "version": 3, "weekStart": "" } diff --git a/src/data/BackendCounters.cpp b/src/data/BackendCounters.cpp index e81c1c34..a501c477 100644 --- a/src/data/BackendCounters.cpp +++ b/src/data/BackendCounters.cpp @@ -24,6 +24,18 @@ namespace data { +namespace { + +std::vector const histogramBuckets{1, 2, 5, 10, 20, 50, 100, 200, 500, 700, 1000}; + +std::int64_t +durationInMillisecondsSince(std::chrono::steady_clock::time_point const startTime) +{ + return std::chrono::duration_cast(std::chrono::steady_clock::now() - startTime).count(); +} + +} // namespace + using namespace util::prometheus; BackendCounters::BackendCounters() @@ -44,6 +56,18 @@ BackendCounters::BackendCounters() )) , asyncWriteCounters_{"write_async"} , asyncReadCounters_{"read_async"} + , readDurationHistogram_(PrometheusService::histogramInt( + "backend_duration_milliseconds_histogram", + Labels({Label{"operation", "read"}}), + histogramBuckets, + "The duration of backend read operations including retries" + )) + , writeDurationHistogram_(PrometheusService::histogramInt( + "backend_duration_milliseconds_histogram", + Labels({Label{"operation", "write"}}), + histogramBuckets, + "The duration of backend write operations including retries" + )) { } @@ -61,9 +85,10 @@ BackendCounters::registerTooBusy() } void -BackendCounters::registerWriteSync() +BackendCounters::registerWriteSync(std::chrono::steady_clock::time_point const startTime) { ++writeSyncCounter_.get(); + writeDurationHistogram_.get().observe(durationInMillisecondsSince(startTime)); } void @@ -79,9 +104,11 @@ BackendCounters::registerWriteStarted() } void -BackendCounters::registerWriteFinished() +BackendCounters::registerWriteFinished(std::chrono::steady_clock::time_point const startTime) { asyncWriteCounters_.registerFinished(1u); + auto const duration = durationInMillisecondsSince(startTime); + writeDurationHistogram_.get().observe(duration); } void @@ -97,9 +124,12 @@ BackendCounters::registerReadStarted(std::uint64_t const count) } void -BackendCounters::registerReadFinished(std::uint64_t const count) +BackendCounters::registerReadFinished(std::chrono::steady_clock::time_point const startTime, std::uint64_t const count) { asyncReadCounters_.registerFinished(count); + auto const duration = durationInMillisecondsSince(startTime); + for (std::uint64_t i = 0; i < count; ++i) + readDurationHistogram_.get().observe(duration); } void diff --git a/src/data/BackendCounters.h b/src/data/BackendCounters.h index 297c052d..575cc77d 100644 --- a/src/data/BackendCounters.h +++ b/src/data/BackendCounters.h @@ -33,23 +33,43 @@ namespace data { /** * @brief A concept for a class that can be used to count backend operations. */ -// clang-format off template concept SomeBackendCounters = requires(T a) { typename T::PtrType; - { a.registerTooBusy() } -> std::same_as; - { a.registerWriteSync() } -> std::same_as; - { a.registerWriteSyncRetry() } -> std::same_as; - { a.registerWriteStarted() } -> std::same_as; - { a.registerWriteFinished() } -> std::same_as; - { a.registerWriteRetry() } -> std::same_as; - { a.registerReadStarted(std::uint64_t{}) } -> std::same_as; - { a.registerReadFinished(std::uint64_t{}) } -> std::same_as; - { a.registerReadRetry(std::uint64_t{}) } -> std::same_as; - { a.registerReadError(std::uint64_t{}) } -> std::same_as; - { a.report() } -> std::same_as; + { + a.registerTooBusy() + } -> std::same_as; + { + a.registerWriteSync(std::chrono::steady_clock::time_point{}) + } -> std::same_as; + { + a.registerWriteSyncRetry() + } -> std::same_as; + { + a.registerWriteStarted() + } -> std::same_as; + { + a.registerWriteFinished(std::chrono::steady_clock::time_point{}) + } -> std::same_as; + { + a.registerWriteRetry() + } -> std::same_as; + { + a.registerReadStarted(std::uint64_t{}) + } -> std::same_as; + { + a.registerReadFinished(std::chrono::steady_clock::time_point{}, std::uint64_t{}) + } -> std::same_as; + { + a.registerReadRetry(std::uint64_t{}) + } -> std::same_as; + { + a.registerReadError(std::uint64_t{}) + } -> std::same_as; + { + a.report() + } -> std::same_as; }; -// clang-format on /** * @brief Holds statistics about the backend. @@ -67,7 +87,7 @@ public: registerTooBusy(); void - registerWriteSync(); + registerWriteSync(std::chrono::steady_clock::time_point startTime); void registerWriteSyncRetry(); @@ -76,7 +96,7 @@ public: registerWriteStarted(); void - registerWriteFinished(); + registerWriteFinished(std::chrono::steady_clock::time_point startTime); void registerWriteRetry(); @@ -85,7 +105,7 @@ public: registerReadStarted(std::uint64_t count = 1u); void - registerReadFinished(std::uint64_t count = 1u); + registerReadFinished(std::chrono::steady_clock::time_point startTime, std::uint64_t count = 1u); void registerReadRetry(std::uint64_t count = 1u); @@ -133,6 +153,9 @@ private: AsyncOperationCounters asyncWriteCounters_{"write_async"}; AsyncOperationCounters asyncReadCounters_{"read_async"}; + + std::reference_wrapper readDurationHistogram_; + std::reference_wrapper writeDurationHistogram_; }; } // namespace data diff --git a/src/data/cassandra/impl/ExecutionStrategy.h b/src/data/cassandra/impl/ExecutionStrategy.h index 9dafd494..2c7604da 100644 --- a/src/data/cassandra/impl/ExecutionStrategy.h +++ b/src/data/cassandra/impl/ExecutionStrategy.h @@ -141,10 +141,11 @@ public: ResultOrErrorType writeSync(StatementType const& statement) { - counters_->registerWriteSync(); + auto const startTime = std::chrono::steady_clock::now(); while (true) { auto res = handle_.get().execute(statement); if (res) { + counters_->registerWriteSync(startTime); return res; } @@ -179,6 +180,8 @@ public: void write(PreparedStatementType const& preparedStatement, Args&&... args) { + auto const startTime = std::chrono::steady_clock::now(); + auto statement = preparedStatement.bind(std::forward(args)...); incrementOutstandingRequestCount(); @@ -188,10 +191,10 @@ public: ioc_, handle_, std::move(statement), - [this](auto const&) { + [this, startTime](auto const&) { decrementOutstandingRequestCount(); - counters_->registerWriteFinished(); + counters_->registerWriteFinished(startTime); }, [this]() { counters_->registerWriteRetry(); } ); @@ -211,6 +214,8 @@ public: if (statements.empty()) return; + auto const startTime = std::chrono::steady_clock::now(); + incrementOutstandingRequestCount(); counters_->registerWriteStarted(); @@ -219,9 +224,9 @@ public: ioc_, handle_, std::move(statements), - [this](auto const&) { + [this, startTime](auto const&) { decrementOutstandingRequestCount(); - counters_->registerWriteFinished(); + counters_->registerWriteFinished(startTime); }, [this]() { counters_->registerWriteRetry(); } ); @@ -258,6 +263,8 @@ public: [[maybe_unused]] ResultOrErrorType read(CompletionTokenType token, std::vector const& statements) { + auto const startTime = std::chrono::steady_clock::now(); + auto const numStatements = statements.size(); std::optional future; counters_->registerReadStarted(numStatements); @@ -283,7 +290,7 @@ public: numReadRequestsOutstanding_ -= numStatements; if (res) { - counters_->registerReadFinished(numStatements); + counters_->registerReadFinished(startTime, numStatements); return res; } @@ -311,6 +318,8 @@ public: [[maybe_unused]] ResultOrErrorType read(CompletionTokenType token, StatementType const& statement) { + auto const startTime = std::chrono::steady_clock::now(); + std::optional future; counters_->registerReadStarted(); @@ -334,7 +343,7 @@ public: --numReadRequestsOutstanding_; if (res) { - counters_->registerReadFinished(); + counters_->registerReadFinished(startTime); return res; } @@ -363,6 +372,8 @@ public: std::vector readEach(CompletionTokenType token, std::vector const& statements) { + auto const startTime = std::chrono::steady_clock::now(); + std::atomic_uint64_t errorsCount = 0u; std::atomic_int numOutstanding = statements.size(); numReadRequestsOutstanding_ += statements.size(); @@ -403,10 +414,10 @@ public: if (errorsCount > 0) { ASSERT(errorsCount <= statements.size(), "Errors number cannot exceed statements number"); counters_->registerReadError(errorsCount); - counters_->registerReadFinished(statements.size() - errorsCount); + counters_->registerReadFinished(startTime, statements.size() - errorsCount); throw DatabaseTimeout{}; } - counters_->registerReadFinished(statements.size()); + counters_->registerReadFinished(startTime, statements.size()); std::vector results; results.reserve(futures.size()); diff --git a/src/etl/ETLService.cpp b/src/etl/ETLService.cpp index f7083777..24ab4291 100644 --- a/src/etl/ETLService.cpp +++ b/src/etl/ETLService.cpp @@ -36,11 +36,14 @@ ETLService::runETLPipeline(uint32_t startSequence, uint32_t numExtractors) LOG(log_.debug()) << "Starting etl pipeline"; state_.isWriting = true; - auto rng = backend_->hardFetchLedgerRangeNoThrow(); - if (!rng || rng->maxSequence < startSequence - 1) { - assert(false); - throw std::runtime_error("runETLPipeline: parent ledger is null"); - } + auto const rng = backend_->hardFetchLedgerRangeNoThrow(); + ASSERT(rng.has_value(), "Parent ledger range can't be null"); + ASSERT( + rng->maxSequence < startSequence - 1, + "Got not parent ledger. rnd->maxSequence = {}, startSequence = {}", + rng->maxSequence, + startSequence + ); auto const begin = std::chrono::system_clock::now(); auto extractors = std::vector>{}; diff --git a/src/etl/Source.h b/src/etl/Source.h index 7042f73d..d3ed038e 100644 --- a/src/etl/Source.h +++ b/src/etl/Source.h @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -485,7 +486,7 @@ public: std::vector edgeKeys; while (numFinished < calls.size() && cq.Next(&tag, &ok)) { - assert(tag); + ASSERT(tag != nullptr, "Tag can't be null."); auto ptr = static_cast(tag); if (!ok) { diff --git a/src/feed/SubscriptionManager.h b/src/feed/SubscriptionManager.h index 3250b752..e7f2402e 100644 --- a/src/feed/SubscriptionManager.h +++ b/src/feed/SubscriptionManager.h @@ -26,6 +26,7 @@ #include #include +#include #include diff --git a/src/util/Atomic.h b/src/util/Atomic.h new file mode 100644 index 00000000..229a4e6b --- /dev/null +++ b/src/util/Atomic.h @@ -0,0 +1,104 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include +#include + +namespace util { + +/** + * @brief Atomic wrapper for integral and floating point types + */ +template +class Atomic { +public: + using ValueType = NumberType; + + Atomic() = default; + Atomic(ValueType const value) : value_(value) + { + } + + ~Atomic() = default; + + // Copy and move constructors and assignment operators are not allowed for atomics + Atomic(Atomic const&) = delete; + Atomic(Atomic&&) = delete; + Atomic& + operator=(Atomic const&) = delete; + Atomic& + operator=(Atomic&&) = delete; + + /** + * @brief Add a value to the current value + * + * @param value The value to add + */ + void + add(ValueType const value) + { + if constexpr (std::is_integral_v) { + 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 + // compare_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 + } + } + + /** + * @brief Update the current value to the new value + * + * @param value The new value + */ + void + set(ValueType const value) + { + value_ = value; + } + + /** + * @brief Get the current value + * + * @return ValueType The current value + */ + ValueType + value() const + { + return value_; + } + +private: + std::atomic value_{0}; +}; + +template +using AtomicPtr = std::unique_ptr>; + +} // namespace util diff --git a/src/util/Concepts.h b/src/util/Concepts.h new file mode 100644 index 00000000..6db150c9 --- /dev/null +++ b/src/util/Concepts.h @@ -0,0 +1,29 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace util { + +template +concept SomeNumberType = std::is_arithmetic_v && !std::is_same_v && !std::is_const_v; + +} // namespace util diff --git a/src/util/prometheus/Counter.h b/src/util/prometheus/Counter.h index fa71348e..66d5d922 100644 --- a/src/util/prometheus/Counter.h +++ b/src/util/prometheus/Counter.h @@ -20,7 +20,7 @@ #pragma once #include -#include +#include #include namespace util::prometheus { @@ -28,7 +28,7 @@ namespace util::prometheus { /** * @brief A prometheus counter metric implementation. It can only be increased or be reset to zero. */ -template +template struct AnyCounter : MetricBase, detail::AnyCounterBase { using ValueType = NumberType; @@ -91,12 +91,12 @@ struct AnyCounter : MetricBase, detail::AnyCounterBase { /** * @brief Serialize the counter to a string in prometheus format (i.e. name{labels} value) * - * @param result The string to serialize into + * @param stream The stream to serialize into */ void - serializeValue(std::string& result) const override + serializeValue(OStream& stream) const override { - fmt::format_to(std::back_inserter(result), "{}{} {}", this->name(), this->labelsString(), value()); + stream << name() << labelsString() << ' ' << value(); } }; diff --git a/src/util/prometheus/Gauge.h b/src/util/prometheus/Gauge.h index d140e499..58c5736d 100644 --- a/src/util/prometheus/Gauge.h +++ b/src/util/prometheus/Gauge.h @@ -19,7 +19,7 @@ #pragma once -#include +#include #include namespace util::prometheus { @@ -27,7 +27,7 @@ namespace util::prometheus { /** * @brief A prometheus gauge metric implementation. It can be increased, decreased or set to a value. */ -template +template struct AnyGauge : MetricBase, detail::AnyCounterBase { using ValueType = NumberType; @@ -113,12 +113,12 @@ struct AnyGauge : MetricBase, detail::AnyCounterBase { /** * @brief Serialize the counter to a string in prometheus format (i.e. name{labels} value) * - * @param result The string to serialize into + * @param stream The stream to serialize into */ void - serializeValue(std::string& result) const override + serializeValue(OStream& stream) const override { - fmt::format_to(std::back_inserter(result), "{}{} {}", this->name(), this->labelsString(), value()); + stream << name() << labelsString() << ' ' << value(); } }; diff --git a/src/util/prometheus/Histogram.h b/src/util/prometheus/Histogram.h new file mode 100644 index 00000000..f458c4c8 --- /dev/null +++ b/src/util/prometheus/Histogram.h @@ -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 +#include +#include + +namespace util::prometheus { + +/** + * @brief A Prometheus histogram metric with a generic value type + */ +template +class AnyHistogram : public MetricBase { +public: + using ValueType = NumberType; + using Buckets = std::vector; + + /** + * @brief Construct a new Histogram object + * + * @param name The name of the metric + * @param labelsString The labels of the metric in serialized format, e.g. {name="value",name2="value2"} + * @param buckets The buckets of the histogram + * @param impl The implementation of the histogram (has default value and need to be specified only for testing) + */ + template > + requires std::same_as::ValueType> + AnyHistogram(std::string name, std::string labelsString, Buckets const& buckets, ImplType&& impl = ImplType{}) + : MetricBase(std::move(name), std::move(labelsString)) + , pimpl_(std::make_unique>(std::forward(impl))) + { + ASSERT(!buckets.empty(), "Histogram must have at least one bucket."); + ASSERT(std::is_sorted(buckets.begin(), buckets.end()), "Buckets for histogra must be sorted."); + pimpl_->setBuckets(buckets); + } + + /** + * @brief Add a value to the histogram + * + * @param value The value to add + */ + void + observe(ValueType const value) + { + pimpl_->observe(value); + } + + /** + * @brief Serialize the metric to a string in Prometheus format + * + * @param stream The stream to serialize into + */ + void + serializeValue(OStream& stream) const override + { + pimpl_->serializeValue(name(), labelsString(), stream); + } + +private: + struct Concept { + virtual ~Concept() = default; + + virtual void observe(NumberType) = 0; + + virtual void + setBuckets(Buckets const& buckets) = 0; + + virtual void + serializeValue(std::string const& name, std::string const& labelsString, OStream&) const = 0; + }; + + template + requires std::same_as::ValueType> + struct Model : Concept { + template + requires std::same_as + Model(SomeImplType&& impl) : impl_(std::forward(impl)) + { + } + + void + observe(NumberType value) override + { + impl_.observe(value); + } + + void + setBuckets(Buckets const& buckets) override + { + impl_.setBuckets(buckets); + } + + void + serializeValue(std::string const& name, std::string const& labelsString, OStream& stream) const override + { + impl_.serializeValue(name, labelsString, stream); + } + + private: + ImplType impl_; + }; + + std::unique_ptr pimpl_; +}; + +using HistogramInt = AnyHistogram; +using HistogramDouble = AnyHistogram; + +} // namespace util::prometheus diff --git a/src/util/prometheus/Http.cpp b/src/util/prometheus/Http.cpp index 24f39b14..c9533f60 100644 --- a/src/util/prometheus/Http.cpp +++ b/src/util/prometheus/Http.cpp @@ -54,8 +54,14 @@ handlePrometheusRequest(http::request const& req, bool const } auto response = http::response(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 + + response.body() = PrometheusService::collectMetrics(); + + if (PrometheusService::compressReplyEnabled()) + response.set(http::field::content_encoding, "gzip"); + return response; } diff --git a/src/util/prometheus/MetricBase.cpp b/src/util/prometheus/MetricBase.cpp new file mode 100644 index 00000000..8f4f7dab --- /dev/null +++ b/src/util/prometheus/MetricBase.cpp @@ -0,0 +1,74 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include + +namespace util::prometheus { + +MetricBase::MetricBase(std::string name, std::string labelsString) + : name_(std::move(name)), labelsString_(std::move(labelsString)) +{ +} + +OStream& +operator<<(OStream& stream, MetricBase const& metricBase) +{ + metricBase.serializeValue(stream); + return stream; +} + +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_INT: + [[fallthrough]]; + case MetricType::HISTOGRAM_DOUBLE: + return "histogram"; + case MetricType::SUMMARY: + return "summary"; + default: + ASSERT(false, "Unknown metric {}.", static_cast(type)); + } + return ""; +} + +std::string const& +MetricBase::name() const +{ + return name_; +} + +std::string const& +MetricBase::labelsString() const +{ + return labelsString_; +} + +} // namespace util::prometheus diff --git a/src/util/prometheus/MetricBase.h b/src/util/prometheus/MetricBase.h new file mode 100644 index 00000000..ea725e57 --- /dev/null +++ b/src/util/prometheus/MetricBase.h @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include + +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 stream The stream to serialize into + * @param metricBase The metric to serialize + */ + friend OStream& + operator<<(OStream& stream, MetricBase const& metricBase); + + /** + * @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 + * + * @return The serialized value + */ + virtual void + serializeValue(OStream& stream) const = 0; + +private: + std::string name_; + std::string labelsString_; +}; + +enum class MetricType { + COUNTER_INT, + COUNTER_DOUBLE, + GAUGE_INT, + GAUGE_DOUBLE, + HISTOGRAM_INT, + HISTOGRAM_DOUBLE, + SUMMARY +}; + +char const* +toString(MetricType type); + +} // namespace util::prometheus diff --git a/src/util/prometheus/MetricBuilder.cpp b/src/util/prometheus/MetricBuilder.cpp new file mode 100644 index 00000000..bd081097 --- /dev/null +++ b/src/util/prometheus/MetricBuilder.cpp @@ -0,0 +1,122 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include + +namespace util::prometheus { + +std::unique_ptr +MetricBuilder::operator()( + std::string name, + std::string labelsString, + MetricType const type, + std::vector const& buckets +) +{ + ASSERT(type != MetricType::HISTOGRAM_DOUBLE, "Wrong metric type. Probably wrong bucket type was used."); + if (type == MetricType::HISTOGRAM_INT) { + return makeHistogram(std::move(name), std::move(labelsString), type, buckets); + } + ASSERT(buckets.empty(), "Buckets must be empty for non-histogram types."); + return makeMetric(std::move(name), std::move(labelsString), type); +} + +std::unique_ptr +MetricBuilder::operator()( + std::string name, + std::string labelsString, + MetricType const type, + std::vector const& buckets +) +{ + ASSERT(type == MetricType::HISTOGRAM_DOUBLE, "This method is for HISTOGRAM_DOUBLE only."); + return makeHistogram(std::move(name), std::move(labelsString), type, buckets); +} + +std::unique_ptr +MetricBuilder::makeMetric(std::string name, std::string labelsString, MetricType const type) +{ + switch (type) { + case MetricType::COUNTER_INT: + return std::make_unique(name, labelsString); + case MetricType::COUNTER_DOUBLE: + return std::make_unique(name, labelsString); + case MetricType::GAUGE_INT: + return std::make_unique(name, labelsString); + case MetricType::GAUGE_DOUBLE: + return std::make_unique(name, labelsString); + case MetricType::HISTOGRAM_INT: + [[fallthrough]]; + case MetricType::HISTOGRAM_DOUBLE: + [[fallthrough]]; + case MetricType::SUMMARY: + [[fallthrough]]; + default: + ASSERT(false, "Unknown metric type: {}", static_cast(type)); + } + return nullptr; +} + +template + requires std::same_as || std::same_as +std::unique_ptr +MetricBuilder::makeHistogram( + std::string name, + std::string labelsString, + MetricType type, + std::vector const& buckets +) +{ + switch (type) { + case MetricType::HISTOGRAM_INT: { + if constexpr (std::same_as) { + return std::make_unique(std::move(name), std::move(labelsString), buckets); + } else { + ASSERT(false, "Wrong bucket type for HISTOGRAM_INT.)"); + break; + } + } + case MetricType::HISTOGRAM_DOUBLE: + if constexpr (std::same_as) { + return std::make_unique(std::move(name), std::move(labelsString), buckets); + } else { + ASSERT(false, "Wrong bucket type for HISTOGRAM_DOUBLE."); + break; + } + case MetricType::COUNTER_INT: + [[fallthrough]]; + case MetricType::COUNTER_DOUBLE: + [[fallthrough]]; + case MetricType::GAUGE_INT: + [[fallthrough]]; + case MetricType::GAUGE_DOUBLE: + [[fallthrough]]; + case MetricType::SUMMARY: + [[fallthrough]]; + default: + ASSERT(false, "Unknown metric type: {}", static_cast(type)); + } + return nullptr; +} + +} // namespace util::prometheus diff --git a/src/util/prometheus/MetricBuilder.h b/src/util/prometheus/MetricBuilder.h new file mode 100644 index 00000000..78bb0683 --- /dev/null +++ b/src/util/prometheus/MetricBuilder.h @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace util::prometheus { + +/** + * @brief Interface to create a metric + */ +struct MetricBuilderInterface { + virtual ~MetricBuilderInterface() = default; + + /** + * @brief Create a metric + * + * @param name The name of the metric + * @param labelsString The labels of the metric in serialized format, e.g. {name="value",name2="value2"} + * @param type The type of the metric + * @param buckets The buckets of the int based histogram. It is ignored for other metric types + * @return The metric + */ + virtual std::unique_ptr + operator()( + std::string name, + std::string labelsString, + MetricType type, + std::vector const& buckets = {} + ) = 0; + + /** + * @brief Create a metric double based histogram + * + * @param name The name of the metric + * @param labelsString The labels of the metric in serialized format, e.g. {name="value",name2="value2"} + * @param type The type of the metric. Must be HISOTGRAM_DOUBLE + * @param buckets The buckets of the histogram + * @return Double based histogram + */ + virtual std::unique_ptr + operator()(std::string name, std::string labelsString, MetricType type, std::vector const& buckets) = 0; +}; + +class MetricBuilder : public MetricBuilderInterface { +public: + std::unique_ptr + operator()( + std::string name, + std::string labelsString, + MetricType type, + std::vector const& buckets = {} + ) override; + + std::unique_ptr + operator()(std::string name, std::string labelsString, MetricType type, std::vector const& buckets) + override; + +private: + std::unique_ptr static makeMetric(std::string name, std::string labelsString, MetricType type); + + template + requires std::same_as || std::same_as + std::unique_ptr + makeHistogram(std::string name, std::string labelsString, MetricType type, std::vector const& buckets); +}; + +} // namespace util::prometheus diff --git a/src/util/prometheus/Metrics.cpp b/src/util/prometheus/Metrics.cpp deleted file mode 100644 index bd01f6f5..00000000 --- a/src/util/prometheus/Metrics.cpp +++ /dev/null @@ -1,140 +0,0 @@ -//------------------------------------------------------------------------------ -/* - 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 -#include - -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, "Unknown metric type: {}", static_cast(type)); - } - 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 { - switch (type) { - case MetricType::COUNTER_INT: - return std::make_unique(name, labelsString); - case MetricType::COUNTER_DOUBLE: - return std::make_unique(name, labelsString); - case MetricType::GAUGE_INT: - return std::make_unique(name, labelsString); - case MetricType::GAUGE_DOUBLE: - return std::make_unique(name, labelsString); - case MetricType::SUMMARY: - [[fallthrough]]; - case MetricType::HISTOGRAM: - [[fallthrough]]; - default: - ASSERT(false, "Unknown metric type: {}", static_cast(type)); - } - return nullptr; -}; - -MetricsFamily::MetricsFamily( - std::string name, - std::optional 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 diff --git a/src/util/prometheus/MetricsFamily.cpp b/src/util/prometheus/MetricsFamily.cpp new file mode 100644 index 00000000..9d83be4e --- /dev/null +++ b/src/util/prometheus/MetricsFamily.cpp @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include + +namespace util::prometheus { + +std::unique_ptr MetricsFamily::defaultMetricBuilder = std::make_unique(); + +MetricsFamily::MetricsFamily( + std::string name, + std::optional description, + MetricType type, + MetricBuilderInterface& metricBuilder +) + : name_(std::move(name)), description_(std::move(description)), type_(type), metricBuilder_(metricBuilder) +{ +} + +MetricBase& +MetricsFamily::getMetric(Labels labels, std::vector const& buckets) +{ + return getMetricImpl(std::move(labels), buckets); +} + +MetricBase& +MetricsFamily::getMetric(Labels labels, std::vector const& buckets) +{ + ASSERT(type_ == MetricType::HISTOGRAM_DOUBLE, "This method is for HISTOGRAM_DOUBLE only."); + return getMetricImpl(std::move(labels), buckets); +} + +OStream& +operator<<(OStream& stream, MetricsFamily const& metricsFamily) +{ + if (metricsFamily.description_) + stream << "# HELP " << metricsFamily.name_ << ' ' << *metricsFamily.description_ << '\n'; + stream << "# TYPE " << metricsFamily.name_ << ' ' << toString(metricsFamily.type()) << '\n'; + + for (auto const& [labelsString, metric] : metricsFamily.metrics_) { + stream << *metric << '\n'; + } + stream << '\n'; + + return stream; +} + +std::string const& +MetricsFamily::name() const +{ + return name_; +} + +MetricType +MetricsFamily::type() const +{ + return type_; +} + +template + requires std::same_as || std::same_as +MetricBase& +MetricsFamily::getMetricImpl(Labels labels, std::vector const& buckets) +{ + auto labelsString = labels.serialize(); + auto it = metrics_.find(labelsString); + if (it == metrics_.end()) { + auto metric = metricBuilder_(name(), labelsString, type(), buckets); + auto [it2, success] = metrics_.emplace(std::move(labelsString), std::move(metric)); + it = it2; + } + return *it->second; +} + +} // namespace util::prometheus diff --git a/src/util/prometheus/Metrics.h b/src/util/prometheus/MetricsFamily.h similarity index 55% rename from src/util/prometheus/Metrics.h rename to src/util/prometheus/MetricsFamily.h index b15cf074..0eb0a991 100644 --- a/src/util/prometheus/Metrics.h +++ b/src/util/prometheus/MetricsFamily.h @@ -19,84 +19,25 @@ #pragma once -#include +#include -#include - -#include -#include #include #include 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::string, std::string, MetricType)>; - static MetricBuilder defaultMetricBuilder; + static std::unique_ptr defaultMetricBuilder; MetricsFamily( std::string name, std::optional description, MetricType type, - MetricBuilder& builder = defaultMetricBuilder + MetricBuilderInterface& builder = *defaultMetricBuilder ); MetricsFamily(MetricsFamily const&) = delete; @@ -110,18 +51,32 @@ public: * @brief Get the metric with the given labels. If it does not exist, it will be created * * @param labels The labels of the metric + * @param buckets The buckets of the histogram. It is ignored for other metric types or if the metric already exists * @return Reference to the metric */ MetricBase& - getMetric(Labels labels); + getMetric(Labels labels, std::vector const& buckets = {}); /** - * @brief Serialize all the containing metrics to a string in Prometheus format as one block + * @brief Get the metric with the given labels. If it does not exist, it will be created * - * @param result The string to serialize into + * @note This overload is only used for histograms with integer buckets + * + * @param labels The labels of the metric + * @param buckets The buckets of the histogram. It is ignored for other metric types or if the metric already exists + * @return Reference to the metric */ - void - serialize(std::string& result) const; + MetricBase& + getMetric(Labels labels, std::vector const& buckets); + + /** + * @brief Serialize the metrics to a string in Prometheus format as one block + * + * @param stream The stream to serialize into + * @param metricsFamily The metrics to serialize + */ + friend OStream& + operator<<(OStream& stream, MetricsFamily const& metricsFamily); std::string const& name() const; @@ -134,7 +89,12 @@ private: std::optional description_; std::unordered_map> metrics_; MetricType type_; - MetricBuilder& metricBuilder_; + MetricBuilderInterface& metricBuilder_; + + template + requires std::same_as || std::same_as + MetricBase& + getMetricImpl(Labels labels, std::vector const& buckets); }; } // namespace util::prometheus diff --git a/src/util/prometheus/OStream.cpp b/src/util/prometheus/OStream.cpp new file mode 100644 index 00000000..d9a05e47 --- /dev/null +++ b/src/util/prometheus/OStream.cpp @@ -0,0 +1,41 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include + +namespace util::prometheus { +OStream::OStream(bool const compressionEnabled) : compressionEnabled_(compressionEnabled) +{ + if (compressionEnabled_) { + stream_.push(boost::iostreams::gzip_compressor{ + boost::iostreams::gzip_params{boost::iostreams::gzip::best_compression}}); + } + stream_.push(boost::iostreams::back_inserter(buffer_)); +} + +std::string +OStream::data() && +{ + stream_.reset(); + return std::move(buffer_); +} + +} // namespace util::prometheus diff --git a/src/util/prometheus/OStream.h b/src/util/prometheus/OStream.h new file mode 100644 index 00000000..39582917 --- /dev/null +++ b/src/util/prometheus/OStream.h @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include + +namespace util::prometheus { + +/** + * @brief A stream that can optionally compress its data + */ +class OStream { +public: + /** + * @brief Construct a new OStream object + * + * @param compressionEnabled Whether to compress the data + */ + OStream(bool compressionEnabled); + + OStream(OStream const&) = delete; + OStream(OStream&&) = delete; + ~OStream() = default; + + /** + * @brief Write to the stream + */ + template + OStream& + operator<<(T const& value) + { + stream_ << value; + return *this; + } + + /** + * @brief Get the data from the stream. + * + * @note This resets the stream and clears the buffer. Stream cannot be used after this. + * + * @return The data + */ + std::string + data() &&; + +private: + bool compressionEnabled_; + std::string buffer_; + boost::iostreams::filtering_ostream stream_; +}; + +} // namespace util::prometheus diff --git a/src/util/prometheus/Prometheus.cpp b/src/util/prometheus/Prometheus.cpp index d48428ee..6b5d8f69 100644 --- a/src/util/prometheus/Prometheus.cpp +++ b/src/util/prometheus/Prometheus.cpp @@ -20,6 +20,9 @@ #include #include +#include +#include + namespace util::prometheus { namespace { @@ -67,18 +70,57 @@ PrometheusImpl::gaugeDouble(std::string name, Labels labels, std::optional(metricBase); } +HistogramInt& +PrometheusImpl::histogramInt( + std::string name, + Labels labels, + std::vector const& buckets, + std::optional description +) +{ + MetricBase& metricBase = + getMetric(std::move(name), std::move(labels), std::move(description), MetricType::HISTOGRAM_INT, buckets); + return convertBaseTo(metricBase); +} + +HistogramDouble& +PrometheusImpl::histogramDouble( + std::string name, + Labels labels, + std::vector const& buckets, + std::optional description +) +{ + MetricBase& metricBase = + getMetric(std::move(name), std::move(labels), std::move(description), MetricType::HISTOGRAM_DOUBLE, buckets); + return convertBaseTo(metricBase); +} + std::string PrometheusImpl::collectMetrics() { - std::string result; - if (!isEnabled()) - return result; + return {}; + + OStream stream{compressReplyEnabled()}; for (auto const& [name, family] : metrics_) { - family.serialize(result); + stream << family; } - return result; + return std::move(stream).data(); +} + +MetricsFamily& +PrometheusImpl::getMetricsFamily(std::string name, std::optional description, MetricType 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; } MetricBase& @@ -89,14 +131,23 @@ PrometheusImpl::getMetric( 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)); + auto& metricFamily = getMetricsFamily(std::move(name), std::move(description), type); + return metricFamily.getMetric(std::move(labels)); +} + +template + requires std::same_as || std::same_as +MetricBase& +PrometheusImpl::getMetric( + std::string name, + Labels labels, + std::optional description, + MetricType type, + std::vector const& buckets +) +{ + auto& metricFamily = getMetricsFamily(std::move(name), std::move(description), type); + return metricFamily.getMetric(std::move(labels), buckets); } } // namespace util::prometheus @@ -104,8 +155,10 @@ PrometheusImpl::getMetric( void PrometheusService::init(util::Config const& config) { - bool const enabled = config.valueOr("prometheus_enabled", true); - instance_ = std::make_unique(enabled); + bool const enabled = config.valueOr("prometheus.enabled", true); + bool const compressReply = config.valueOr("prometheus.compress_reply", true); + + instance_ = std::make_unique(enabled, compressReply); } util::prometheus::CounterInt& @@ -140,6 +193,28 @@ PrometheusService::gaugeDouble( return instance().gaugeDouble(std::move(name), std::move(labels), std::move(description)); } +util::prometheus::HistogramInt& +PrometheusService::histogramInt( + std::string name, + util::prometheus::Labels labels, + std::vector const& buckets, + std::optional description +) +{ + return instance().histogramInt(std::move(name), std::move(labels), buckets, std::move(description)); +} + +util::prometheus::HistogramDouble& +PrometheusService::histogramDouble( + std::string name, + util::prometheus::Labels labels, + std::vector const& buckets, + std::optional description +) +{ + return instance().histogramDouble(std::move(name), std::move(labels), buckets, std::move(description)); +} + std::string PrometheusService::collectMetrics() { @@ -152,6 +227,12 @@ PrometheusService::isEnabled() return instance().isEnabled(); } +bool +PrometheusService::compressReplyEnabled() +{ + return instance().compressReplyEnabled(); +} + void PrometheusService::replaceInstance(std::unique_ptr instance) { diff --git a/src/util/prometheus/Prometheus.h b/src/util/prometheus/Prometheus.h index 995c86a3..e09bd49f 100644 --- a/src/util/prometheus/Prometheus.h +++ b/src/util/prometheus/Prometheus.h @@ -22,6 +22,8 @@ #include #include #include +#include +#include namespace util::prometheus { @@ -32,18 +34,20 @@ public: * * @param isEnabled Whether prometheus is enabled */ - PrometheusInterface(bool isEnabled) : isEnabled_(isEnabled) + PrometheusInterface(bool isEnabled, bool compressReply) + : isEnabled_(isEnabled), compressReplyEnabled_(compressReply) { } virtual ~PrometheusInterface() = default; /** - * @brief Get a integer based counter metric. It will be created if it doesn't exist + * @brief Get an 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 + * @return CounterDouble& The reference to the counter object */ virtual CounterInt& counterInt(std::string name, Labels labels, std::optional description = std::nullopt) = 0; @@ -54,16 +58,18 @@ public: * @param name The name of the metric * @param labels The labels of the metric * @param description The description of the metric + * @return The reference to the counter object */ virtual CounterDouble& counterDouble(std::string name, Labels labels, std::optional description = std::nullopt) = 0; /** - * @brief Get a integer based gauge metric. It will be created if it doesn't exist + * @brief Get an 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 + * @return The reference to the gauge object */ virtual GaugeInt& gaugeInt(std::string name, Labels labels, std::optional description = std::nullopt) = 0; @@ -74,10 +80,45 @@ public: * @param name The name of the metric * @param labels The labels of the metric * @param description The description of the metric + * @return The reference to the gauge object */ virtual GaugeDouble& gaugeDouble(std::string name, Labels labels, std::optional description = std::nullopt) = 0; + /** + * @brief Get an integer based histogram 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 buckets The buckets of the metric + * @param description The description of the metric + * @return The reference to the histogram object + */ + virtual HistogramInt& + histogramInt( + std::string name, + Labels labels, + std::vector const& buckets, + std::optional description = std::nullopt + ) = 0; + + /** + * @brief Get a double based histogram 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 buckets The buckets of the metric + * @param description The description of the metric + * @return The reference to the histogram object + */ + virtual HistogramDouble& + histogramDouble( + std::string name, + Labels labels, + std::vector const& buckets, + std::optional description = std::nullopt + ) = 0; + /** * @brief Collect all metrics and return them as a string in Prometheus format * @@ -97,8 +138,20 @@ public: return isEnabled_; } + /** + * @brief Whether to compress the reply + * + * @return true if the reply should be compressed + */ + bool + compressReplyEnabled() const + { + return compressReplyEnabled_; + } + private: bool isEnabled_; + bool compressReplyEnabled_; }; /** @@ -122,13 +175,43 @@ public: GaugeDouble& gaugeDouble(std::string name, Labels labels, std::optional description) override; + HistogramInt& + histogramInt( + std::string name, + Labels labels, + std::vector const& buckets, + std::optional description = std::nullopt + ) override; + + HistogramDouble& + histogramDouble( + std::string name, + Labels labels, + std::vector const& buckets, + std::optional description = std::nullopt + ) override; + std::string collectMetrics() override; private: + MetricsFamily& + getMetricsFamily(std::string name, std::optional description, MetricType type); + MetricBase& getMetric(std::string name, Labels labels, std::optional description, MetricType type); + template + requires std::same_as || std::same_as + MetricBase& + getMetric( + std::string name, + Labels labels, + std::optional description, + MetricType type, + std::vector const& buckets + ); + std::unordered_map metrics_; }; @@ -147,7 +230,7 @@ public: 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 + * @brief Get an 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 @@ -175,7 +258,7 @@ public: ); /** - * @brief Get a integer based gauge metric. It will be created if it doesn't exist + * @brief Get an 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 @@ -198,6 +281,40 @@ public: std::optional description = std::nullopt ); + /** + * @brief Get an integer based histogram 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 buckets The buckets of the metric + * @param description The description of the metric + * @return The reference to the histogram object + */ + static util::prometheus::HistogramInt& + histogramInt( + std::string name, + util::prometheus::Labels labels, + std::vector const& buckets, + std::optional description = std::nullopt + ); + + /** + * @brief Get a double based histogram 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 buckets The buckets of the metric + * @param description The description of the metric + * @return The reference to the histogram object + */ + static util::prometheus::HistogramDouble& + histogramDouble( + std::string name, + util::prometheus::Labels labels, + std::vector const& buckets, + std::optional description = std::nullopt + ); + /** * @brief Collect all metrics and return them as a string in Prometheus format * @@ -214,6 +331,14 @@ public: static bool isEnabled(); + /** + * @brief Whether to compress the reply + * + * @return true if the reply should be compressed + */ + static bool + compressReplyEnabled(); + /** * @brief Replace the prometheus object stored in the singleton * diff --git a/src/util/prometheus/impl/AnyCounterBase.h b/src/util/prometheus/impl/AnyCounterBase.h index 7b95276f..a418413a 100644 --- a/src/util/prometheus/impl/AnyCounterBase.h +++ b/src/util/prometheus/impl/AnyCounterBase.h @@ -51,7 +51,9 @@ protected: template struct Model : Concept { - Model(ImplType impl) : impl_(std::forward(impl)) + template + requires std::same_as + Model(SomeImplType&& impl) : impl_(std::forward(impl)) { } diff --git a/src/util/prometheus/impl/CounterImpl.h b/src/util/prometheus/impl/CounterImpl.h index aa929a29..0f1b6682 100644 --- a/src/util/prometheus/impl/CounterImpl.h +++ b/src/util/prometheus/impl/CounterImpl.h @@ -20,15 +20,13 @@ #pragma once #include +#include #include #include namespace util::prometheus::detail { -template -concept SomeNumberType = std::is_arithmetic_v && !std::is_same_v && !std::is_const_v; - template concept SomeCounterImpl = requires(T a) { typename std::remove_cvref_t::ValueType; @@ -53,50 +51,33 @@ public: CounterImpl(CounterImpl const&) = delete; - // Move constructor should be used only used during initialization - CounterImpl(CounterImpl&& other) - { - ASSERT(other.value_ == 0, "Move constructor should only be used during initialization"); - value_ = other.value_.exchange(0); - } + CounterImpl(CounterImpl&& other) = default; CounterImpl& operator=(CounterImpl const&) = delete; CounterImpl& - operator=(CounterImpl&&) = delete; + operator=(CounterImpl&&) = default; void add(ValueType const value) { - if constexpr (std::is_integral_v) { - 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 - } + value_->add(value); } void set(ValueType const value) { - value_ = value; + value_->set(value); } ValueType value() const { - return value_; + return value_->value(); } private: - std::atomic value_{0}; + AtomicPtr value_ = std::make_unique>(0); }; } // namespace util::prometheus::detail diff --git a/src/util/prometheus/impl/HistogramImpl.h b/src/util/prometheus/impl/HistogramImpl.h new file mode 100644 index 00000000..189f1cce --- /dev/null +++ b/src/util/prometheus/impl/HistogramImpl.h @@ -0,0 +1,134 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include + +#include + +namespace util::prometheus::detail { + +template +concept SomeHistogramImpl = requires(T t) { + typename std::remove_cvref_t::ValueType; + SomeNumberType::ValueType>; + { + t.observe(typename std::remove_cvref_t::ValueType{1}) + } -> std::same_as; + { + t.setBuckets(std::vector::ValueType>{}) + } -> std::same_as; + { + t.serializeValue(std::string{}, std::string{}, std::declval()) + } -> std::same_as; +}; + +template +class HistogramImpl { +public: + using ValueType = NumberType; + + HistogramImpl() = default; + HistogramImpl(HistogramImpl const&) = delete; + HistogramImpl(HistogramImpl&&) = default; + HistogramImpl& + operator=(HistogramImpl const&) = delete; + HistogramImpl& + operator=(HistogramImpl&&) = default; + + void + setBuckets(std::vector const& bounds) + { + std::scoped_lock const lock{*mutex_}; + ASSERT(buckets_.empty(), "Buckets can be set only once."); + buckets_.reserve(bounds.size()); + for (auto const& bound : bounds) { + buckets_.emplace_back(bound); + } + } + + void + observe(ValueType const value) + { + auto const bucket = + std::lower_bound(buckets_.begin(), buckets_.end(), value, [](Bucket const& bucket, ValueType const& value) { + return bucket.upperBound < value; + }); + std::scoped_lock const lock{*mutex_}; + if (bucket != buckets_.end()) { + ++bucket->count; + } else { + ++lastBucket_.count; + } + sum_ += value; + } + + void + serializeValue(std::string const& name, std::string labelsString, OStream& stream) const + { + if (labelsString.empty()) { + labelsString = "{"; + } else { + ASSERT( + labelsString.front() == '{' && labelsString.back() == '}', + "Labels must be in Prometheus serialized format." + ); + labelsString.back() = ','; + } + + std::scoped_lock const lock{*mutex_}; + std::uint64_t cumulativeCount = 0; + + for (auto const& bucket : buckets_) { + cumulativeCount += bucket.count; + stream << name << "_bucket" << labelsString << "le=\"" << bucket.upperBound << "\"} " << cumulativeCount + << '\n'; + } + cumulativeCount += lastBucket_.count; + stream << name << "_bucket" << labelsString << "le=\"+Inf\"} " << cumulativeCount << '\n'; + + if (labelsString.size() == 1) { + labelsString = ""; + } else { + labelsString.back() = '}'; + } + stream << name << "_sum" << labelsString << " " << sum_ << '\n'; + stream << name << "_count" << labelsString << " " << cumulativeCount << '\n'; + } + +private: + struct Bucket { + Bucket(ValueType upperBound) : upperBound(upperBound) + { + } + + ValueType upperBound; + std::uint64_t count = 0; + }; + + std::vector buckets_; + Bucket lastBucket_{std::numeric_limits::max()}; + ValueType sum_ = 0; + mutable std::unique_ptr mutex_ = std::make_unique(); +}; + +} // namespace util::prometheus::detail diff --git a/unittests/data/BackendCountersTests.cpp b/unittests/data/BackendCountersTests.cpp index bbe967e7..dc6ca016 100644 --- a/unittests/data/BackendCountersTests.cpp +++ b/unittests/data/BackendCountersTests.cpp @@ -48,6 +48,7 @@ struct BackendCountersTest : WithPrometheus { } BackendCounters::PtrType const counters = BackendCounters::make(); + std::chrono::steady_clock::time_point startTime{}; }; TEST_F(BackendCountersTest, EmptyByDefault) @@ -68,8 +69,9 @@ TEST_F(BackendCountersTest, RegisterTooBusy) TEST_F(BackendCountersTest, RegisterWriteSync) { - counters->registerWriteSync(); - counters->registerWriteSync(); + std::chrono::steady_clock::time_point const startTime{}; + counters->registerWriteSync(startTime); + counters->registerWriteSync(startTime); auto expectedReport = emptyReport(); expectedReport["write_sync"] = 2; @@ -102,8 +104,8 @@ TEST_F(BackendCountersTest, RegisterWriteFinished) counters->registerWriteStarted(); counters->registerWriteStarted(); counters->registerWriteStarted(); - counters->registerWriteFinished(); - counters->registerWriteFinished(); + counters->registerWriteFinished(startTime); + counters->registerWriteFinished(startTime); auto expectedReport = emptyReport(); expectedReport["write_async_pending"] = 1; @@ -136,8 +138,8 @@ TEST_F(BackendCountersTest, RegisterReadFinished) counters->registerReadStarted(); counters->registerReadStarted(); counters->registerReadStarted(); - counters->registerReadFinished(); - counters->registerReadFinished(); + counters->registerReadFinished(startTime); + counters->registerReadFinished(startTime); auto expectedReport = emptyReport(); expectedReport["read_async_pending"] = 1; @@ -151,7 +153,7 @@ TEST_F(BackendCountersTest, RegisterReadStartedFinishedWithCounters) static constexpr auto OPERATIONS_COMPLETED = 4u; counters->registerReadStarted(OPERATIONS_STARTED); - counters->registerReadFinished(OPERATIONS_COMPLETED); + counters->registerReadFinished(startTime, OPERATIONS_COMPLETED); auto expectedReport = emptyReport(); expectedReport["read_async_pending"] = OPERATIONS_STARTED - OPERATIONS_COMPLETED; @@ -177,7 +179,7 @@ TEST_F(BackendCountersTest, RegisterReadError) counters->registerReadStarted(OPERATIONS_STARTED); counters->registerReadError(OPERATIONS_ERROR); - counters->registerReadFinished(OPERATIONS_COMPLETED); + counters->registerReadFinished(startTime, OPERATIONS_COMPLETED); auto expectedReport = emptyReport(); expectedReport["read_async_pending"] = OPERATIONS_STARTED - OPERATIONS_COMPLETED - OPERATIONS_ERROR; @@ -200,8 +202,11 @@ TEST_F(BackendCountersMockPrometheusTest, registerTooBusy) TEST_F(BackendCountersMockPrometheusTest, registerWriteSync) { auto& counter = makeMock("backend_operations_total_number", "{operation=\"write_sync\"}"); + auto& histogram = makeMock("backend_duration_milliseconds_histogram", "{operation=\"write\"}"); EXPECT_CALL(counter, add(1)); - counters->registerWriteSync(); + EXPECT_CALL(histogram, observe(testing::_)); + std::chrono::steady_clock::time_point const startTime{}; + counters->registerWriteSync(startTime); } TEST_F(BackendCountersMockPrometheusTest, registerWriteSyncRetry) @@ -225,10 +230,13 @@ TEST_F(BackendCountersMockPrometheusTest, registerWriteFinished) makeMock("backend_operations_current_number", "{operation=\"write_async\",status=\"pending\"}"); auto& completedCounter = makeMock("backend_operations_total_number", "{operation=\"write_async\",status=\"completed\"}"); + auto& histogram = makeMock("backend_duration_milliseconds_histogram", "{operation=\"write\"}"); EXPECT_CALL(pendingCounter, value()).WillOnce(testing::Return(1)); EXPECT_CALL(pendingCounter, add(-1)); EXPECT_CALL(completedCounter, add(1)); - counters->registerWriteFinished(); + EXPECT_CALL(histogram, observe(testing::_)); + std::chrono::steady_clock::time_point const startTime{}; + counters->registerWriteFinished(startTime); } TEST_F(BackendCountersMockPrometheusTest, registerWriteRetry) @@ -253,10 +261,13 @@ TEST_F(BackendCountersMockPrometheusTest, registerReadFinished) makeMock("backend_operations_current_number", "{operation=\"read_async\",status=\"pending\"}"); auto& completedCounter = makeMock("backend_operations_total_number", "{operation=\"read_async\",status=\"completed\"}"); - EXPECT_CALL(pendingCounter, value()).WillOnce(testing::Return(1)); - EXPECT_CALL(pendingCounter, add(-1)); - EXPECT_CALL(completedCounter, add(1)); - counters->registerReadFinished(); + auto& histogram = makeMock("backend_duration_milliseconds_histogram", "{operation=\"read\"}"); + EXPECT_CALL(pendingCounter, value()).WillOnce(testing::Return(2)); + EXPECT_CALL(pendingCounter, add(-2)); + EXPECT_CALL(completedCounter, add(2)); + EXPECT_CALL(histogram, observe(testing::_)).Times(2); + std::chrono::steady_clock::time_point const startTime{}; + counters->registerReadFinished(startTime, 2); } TEST_F(BackendCountersMockPrometheusTest, registerReadRetry) diff --git a/unittests/data/cassandra/ExecutionStrategyTests.cpp b/unittests/data/cassandra/ExecutionStrategyTests.cpp index 8ecb66bd..265968c5 100644 --- a/unittests/data/cassandra/ExecutionStrategyTests.cpp +++ b/unittests/data/cassandra/ExecutionStrategyTests.cpp @@ -40,10 +40,10 @@ protected: } MOCK_METHOD(void, registerTooBusy, (), ()); - MOCK_METHOD(void, registerWriteSync, (), ()); + MOCK_METHOD(void, registerWriteSync, (std::chrono::steady_clock::time_point), ()); MOCK_METHOD(void, registerWriteSyncRetry, (), ()); MOCK_METHOD(void, registerWriteStarted, (), ()); - MOCK_METHOD(void, registerWriteFinished, (), ()); + MOCK_METHOD(void, registerWriteFinished, (std::chrono::steady_clock::time_point), ()); MOCK_METHOD(void, registerWriteRetry, (), ()); void @@ -54,11 +54,11 @@ protected: MOCK_METHOD(void, registerReadStartedImpl, (std::uint64_t), ()); void - registerReadFinished(std::uint64_t count = 1) + registerReadFinished(std::chrono::steady_clock::time_point startTime, std::uint64_t count = 1) { - registerReadFinishedImpl(count); + registerReadFinishedImpl(startTime, count); } - MOCK_METHOD(void, registerReadFinishedImpl, (std::uint64_t), ()); + MOCK_METHOD(void, registerReadFinishedImpl, (std::chrono::steady_clock::time_point, std::uint64_t), ()); void registerReadRetry(std::uint64_t count = 1) @@ -110,7 +110,7 @@ TEST_F(BackendCassandraExecutionStrategyTest, ReadOneInCoroutineSuccessful) EXPECT_CALL(handle, asyncExecute(A(), A&&>())) .Times(1); EXPECT_CALL(*counters, registerReadStartedImpl(1)); - EXPECT_CALL(*counters, registerReadFinishedImpl(1)); + EXPECT_CALL(*counters, registerReadFinishedImpl(testing::_, 1)); runSpawn([&strat](boost::asio::yield_context yield) { auto statement = FakeStatement{}; @@ -175,7 +175,7 @@ TEST_F(BackendCassandraExecutionStrategyTest, ReadBatchInCoroutineSuccessful) ) .Times(1); EXPECT_CALL(*counters, registerReadStartedImpl(NUM_STATEMENTS)); - EXPECT_CALL(*counters, registerReadFinishedImpl(NUM_STATEMENTS)); + EXPECT_CALL(*counters, registerReadFinishedImpl(testing::_, NUM_STATEMENTS)); runSpawn([&strat](boost::asio::yield_context yield) { auto statements = std::vector(NUM_STATEMENTS); @@ -249,7 +249,7 @@ TEST_F(BackendCassandraExecutionStrategyTest, ReadBatchInCoroutineMarksBusyIfReq ) .Times(1); EXPECT_CALL(*counters, registerReadStartedImpl(NUM_STATEMENTS)); - EXPECT_CALL(*counters, registerReadFinishedImpl(NUM_STATEMENTS)); + EXPECT_CALL(*counters, registerReadFinishedImpl(testing::_, NUM_STATEMENTS)); runSpawn([&strat](boost::asio::yield_context yield) { EXPECT_FALSE(strat.isTooBusy()); // 2 was the limit, 0 atm @@ -277,7 +277,7 @@ TEST_F(BackendCassandraExecutionStrategyTest, ReadEachInCoroutineSuccessful) ) .Times(NUM_STATEMENTS); // once per statement EXPECT_CALL(*counters, registerReadStartedImpl(NUM_STATEMENTS)); - EXPECT_CALL(*counters, registerReadFinishedImpl(NUM_STATEMENTS)); + EXPECT_CALL(*counters, registerReadFinishedImpl(testing::_, NUM_STATEMENTS)); runSpawn([&strat](boost::asio::yield_context yield) { auto statements = std::vector(NUM_STATEMENTS); @@ -311,7 +311,7 @@ TEST_F(BackendCassandraExecutionStrategyTest, ReadEachInCoroutineThrowsOnFailure .Times(NUM_STATEMENTS); // once per statement EXPECT_CALL(*counters, registerReadStartedImpl(NUM_STATEMENTS)); EXPECT_CALL(*counters, registerReadErrorImpl(1)); - EXPECT_CALL(*counters, registerReadFinishedImpl(2)); + EXPECT_CALL(*counters, registerReadFinishedImpl(testing::_, 2)); runSpawn([&strat](boost::asio::yield_context yield) { auto statements = std::vector(NUM_STATEMENTS); @@ -326,7 +326,7 @@ TEST_F(BackendCassandraExecutionStrategyTest, WriteSyncFirstTrySuccessful) ON_CALL(handle, execute(A())).WillByDefault([](auto const&) { return FakeResultOrError{}; }); EXPECT_CALL(handle, execute(A())).Times(1); // first one will succeed - EXPECT_CALL(*counters, registerWriteSync()); + EXPECT_CALL(*counters, registerWriteSync(testing::_)); EXPECT_TRUE(strat.writeSync({})); } @@ -344,7 +344,7 @@ TEST_F(BackendCassandraExecutionStrategyTest, WriteSyncRetrySuccessful) EXPECT_CALL(handle, execute(A())).Times(2); // first one will fail, second will succeed EXPECT_CALL(*counters, registerWriteSyncRetry()); - EXPECT_CALL(*counters, registerWriteSync()); + EXPECT_CALL(*counters, registerWriteSync(testing::_)); EXPECT_TRUE(strat.writeSync({})); } @@ -376,7 +376,7 @@ TEST_F(BackendCassandraExecutionStrategyTest, WriteMultipleAndCallSyncSucceeds) ) .Times(totalRequests); // one per write call EXPECT_CALL(*counters, registerWriteStarted()).Times(totalRequests); - EXPECT_CALL(*counters, registerWriteFinished()).Times(totalRequests); + EXPECT_CALL(*counters, registerWriteFinished(testing::_)).Times(totalRequests); auto makeStatements = [] { return std::vector(16); }; for (auto i = 0u; i < totalRequests; ++i) diff --git a/unittests/util/AtomicTests.cpp b/unittests/util/AtomicTests.cpp new file mode 100644 index 00000000..a360c8a5 --- /dev/null +++ b/unittests/util/AtomicTests.cpp @@ -0,0 +1,76 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include + +#include + +using namespace util; + +TEST(AtomicTests, add) +{ + Atomic atomic{42}; + atomic.add(1); + EXPECT_EQ(atomic.value(), 43); +} + +TEST(AtomicTests, set) +{ + Atomic atomic{42}; + atomic.set(1); + EXPECT_EQ(atomic.value(), 1); +} + +TEST(AtomicTest, multithreadAddInt) +{ + Atomic atomic{0}; + std::vector threads; + threads.reserve(100); + for (int i = 0; i < 100; ++i) { + threads.emplace_back([&atomic] { + for (int j = 0; j < 100; ++j) { + atomic.add(1); + } + }); + } + for (auto& thread : threads) { + thread.join(); + } + EXPECT_EQ(atomic.value(), 10000); +} + +TEST(AtomicTest, multithreadAddDouble) +{ + Atomic atomic{0.0}; + std::vector threads; + threads.reserve(100); + for (int i = 0; i < 100; ++i) { + threads.emplace_back([&atomic] { + for (int j = 0; j < 100; ++j) { + atomic.add(1.0); + } + }); + } + for (auto& thread : threads) { + thread.join(); + } + EXPECT_NEAR(atomic.value(), 10000.0, 1e-9); +} diff --git a/unittests/util/MockPrometheus.h b/unittests/util/MockPrometheus.h index 6c2cdbce..c853d279 100644 --- a/unittests/util/MockPrometheus.h +++ b/unittests/util/MockPrometheus.h @@ -26,7 +26,7 @@ namespace util::prometheus { -template +template struct MockCounterImpl { using ValueType = NumberType; @@ -39,8 +39,26 @@ using MockCounterImplInt = MockCounterImpl; using MockCounterImplUint = MockCounterImpl; using MockCounterImplDouble = MockCounterImpl; +template + requires std::same_as || std::same_as +struct MockHistogramImpl { + using ValueType = NumberType; + + MockHistogramImpl() + { + EXPECT_CALL(*this, setBuckets); + } + + MOCK_METHOD(void, observe, (ValueType), ()); + MOCK_METHOD(void, setBuckets, (std::vector const&), ()); + MOCK_METHOD(void, serializeValue, (std::string const&, std::string, OStream&), (const)); +}; + +using MockHistogramImplInt = MockHistogramImpl; +using MockHistogramImplDouble = MockHistogramImpl; + struct MockPrometheusImpl : PrometheusInterface { - MockPrometheusImpl() : PrometheusInterface(true) + MockPrometheusImpl() : PrometheusInterface(true, true) { EXPECT_CALL(*this, counterInt) .WillRepeatedly([this](std::string name, Labels labels, std::optional) -> CounterInt& { @@ -58,12 +76,34 @@ struct MockPrometheusImpl : PrometheusInterface { .WillRepeatedly([this](std::string name, Labels labels, std::optional) -> GaugeDouble& { return getMetric(std::move(name), std::move(labels)); }); + EXPECT_CALL(*this, histogramInt) + .WillRepeatedly( + [this](std::string name, Labels labels, std::vector const&, std::optional) + -> HistogramInt& { return getMetric(std::move(name), std::move(labels)); } + ); + EXPECT_CALL(*this, histogramDouble) + .WillRepeatedly( + [this](std::string name, Labels labels, std::vector const&, std::optional) + -> HistogramDouble& { return getMetric(std::move(name), std::move(labels)); } + ); } MOCK_METHOD(CounterInt&, counterInt, (std::string, Labels, std::optional), (override)); MOCK_METHOD(CounterDouble&, counterDouble, (std::string, Labels, std::optional), (override)); MOCK_METHOD(GaugeInt&, gaugeInt, (std::string, Labels, std::optional), (override)); MOCK_METHOD(GaugeDouble&, gaugeDouble, (std::string, Labels, std::optional), (override)); + MOCK_METHOD( + HistogramInt&, + histogramInt, + (std::string, Labels, std::vector const&, std::optional), + (override) + ); + MOCK_METHOD( + HistogramDouble&, + histogramDouble, + (std::string, Labels, std::vector const&, std::optional), + (override) + ); MOCK_METHOD(std::string, collectMetrics, (), (override)); template @@ -88,15 +128,23 @@ struct MockPrometheusImpl : PrometheusInterface { { std::unique_ptr metric; auto const key = name + labelsString; - if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { auto& impl = counterIntImpls[key]; metric = std::make_unique(name, labelsString, impl); - } else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { auto& impl = counterUintImpls[key]; metric = std::make_unique(name, labelsString, impl); - } else { + } else if constexpr (std::is_same_v || std::is_same_v) { auto& impl = counterDoubleImpls[key]; metric = std::make_unique(name, labelsString, impl); + } else if constexpr (std::is_same_v) { + auto& impl = histogramIntImpls[key]; + metric = std::make_unique(name, labelsString, std::vector{1}, impl); + } else if constexpr (std::is_same_v) { + auto& impl = histogramDoubleImpls[key]; + metric = std::make_unique(name, labelsString, std::vector{1.}, impl); + } else { + throw std::runtime_error("Wrong metric type"); } auto* ptr = metrics.emplace(key, std::move(metric)).first->second.get(); auto metricPtr = dynamic_cast(ptr); @@ -108,6 +156,8 @@ struct MockPrometheusImpl : PrometheusInterface { std::unordered_map> counterIntImpls; std::unordered_map> counterUintImpls; std::unordered_map> counterDoubleImpls; + std::unordered_map> histogramIntImpls; + std::unordered_map> histogramDoubleImpls; }; /** @@ -147,15 +197,22 @@ struct WithMockPrometheus : virtual ::testing::Test { ASSERT(mockPrometheusPtr != nullptr, "Wrong prometheus type"); std::string const key = name + labelsString; - mockPrometheusPtr->makeMetric(std::move(name), std::move(labelsString)); - if constexpr (std::is_same_v) { + + if (!mockPrometheusPtr->metrics.contains(key)) + mockPrometheusPtr->makeMetric(std::move(name), std::move(labelsString)); + + if constexpr (std::is_same_v) { return mockPrometheusPtr->counterIntImpls[key]; - } else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { return mockPrometheusPtr->counterUintImpls[key]; - } else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v || std::is_same_v) { return mockPrometheusPtr->counterDoubleImpls[key]; + } else if constexpr (std::is_same_v) { + return mockPrometheusPtr->histogramIntImpls[key]; + } else if constexpr (std::is_same_v) { + return mockPrometheusPtr->histogramDoubleImpls[key]; } - ASSERT(false, "Wrong metric type"); + ASSERT(false, "Wrong metric type for metric {} {}", name, labelsString); } }; @@ -165,7 +222,8 @@ struct WithMockPrometheus : virtual ::testing::Test { struct WithPrometheus : virtual ::testing::Test { WithPrometheus() { - PrometheusService::init(); + boost::json::value const config{{"prometheus", boost::json::object{{"compress_reply", false}}}}; + PrometheusService::init(Config{config}); } ~WithPrometheus() override diff --git a/unittests/util/prometheus/CounterTests.cpp b/unittests/util/prometheus/CounterTests.cpp index ae3036ea..741e9132 100644 --- a/unittests/util/prometheus/CounterTests.cpp +++ b/unittests/util/prometheus/CounterTests.cpp @@ -19,6 +19,7 @@ #include +#include #include #include @@ -53,9 +54,9 @@ TEST_F(AnyCounterTests, 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)"); + OStream stream{false}; + counter.serializeValue(stream); + EXPECT_EQ(std::move(stream).data(), R"(test_counter{label1="value1",label2="value2"} 42)"); } TEST_F(AnyCounterTests, operatorAdd) diff --git a/unittests/util/prometheus/HistogramTests.cpp b/unittests/util/prometheus/HistogramTests.cpp new file mode 100644 index 00000000..e6b28a42 --- /dev/null +++ b/unittests/util/prometheus/HistogramTests.cpp @@ -0,0 +1,115 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include +#include + +using namespace util::prometheus; + +struct AnyHistogramTests : ::testing::Test { + struct MockHistogramImpl { + MockHistogramImpl() + { + EXPECT_CALL(*this, setBuckets); + } + using ValueType = std::int64_t; + MOCK_METHOD(void, observe, (ValueType)); + MOCK_METHOD(void, setBuckets, (std::vector const&)); + MOCK_METHOD(void, serializeValue, (std::string const&, std::string, OStream&), (const)); + }; + + ::testing::StrictMock mockHistogramImpl; + std::string const name = "test_histogram"; + std::string labelsString = R"({label1="value1",label2="value2"})"; + HistogramInt histogram{name, labelsString, {1, 2, 3}, static_cast(mockHistogramImpl)}; +}; + +TEST_F(AnyHistogramTests, name) +{ + EXPECT_EQ(histogram.name(), name); +} + +TEST_F(AnyHistogramTests, labelsString) +{ + EXPECT_EQ(histogram.labelsString(), labelsString); +} + +TEST_F(AnyHistogramTests, observe) +{ + EXPECT_CALL(mockHistogramImpl, observe(42)); + histogram.observe(42); +} + +TEST_F(AnyHistogramTests, serializeValue) +{ + OStream stream{false}; + EXPECT_CALL(mockHistogramImpl, serializeValue(name, labelsString, ::testing::_)); + histogram.serializeValue(stream); +} + +struct HistogramTests : ::testing::Test { + std::string labelsString = R"({label1="value1",label2="value2"})"; + HistogramInt histogram{"t", labelsString, {1, 2, 3}}; + + std::string + serialize() const + { + OStream stream{false}; + histogram.serializeValue(stream); + return std::move(stream).data(); + } +}; + +TEST_F(HistogramTests, observe) +{ + histogram.observe(0); + EXPECT_EQ( + serialize(), + "t_bucket{label1=\"value1\",label2=\"value2\",le=\"1\"} 1\n" + "t_bucket{label1=\"value1\",label2=\"value2\",le=\"2\"} 1\n" + "t_bucket{label1=\"value1\",label2=\"value2\",le=\"3\"} 1\n" + "t_bucket{label1=\"value1\",label2=\"value2\",le=\"+Inf\"} 1\n" + "t_sum{label1=\"value1\",label2=\"value2\"} 0\n" + "t_count{label1=\"value1\",label2=\"value2\"} 1\n" + ) << serialize(); + + histogram.observe(2); + EXPECT_EQ( + serialize(), + "t_bucket{label1=\"value1\",label2=\"value2\",le=\"1\"} 1\n" + "t_bucket{label1=\"value1\",label2=\"value2\",le=\"2\"} 2\n" + "t_bucket{label1=\"value1\",label2=\"value2\",le=\"3\"} 2\n" + "t_bucket{label1=\"value1\",label2=\"value2\",le=\"+Inf\"} 2\n" + "t_sum{label1=\"value1\",label2=\"value2\"} 2\n" + "t_count{label1=\"value1\",label2=\"value2\"} 2\n" + ); + + histogram.observe(123); + EXPECT_EQ( + serialize(), + "t_bucket{label1=\"value1\",label2=\"value2\",le=\"1\"} 1\n" + "t_bucket{label1=\"value1\",label2=\"value2\",le=\"2\"} 2\n" + "t_bucket{label1=\"value1\",label2=\"value2\",le=\"3\"} 2\n" + "t_bucket{label1=\"value1\",label2=\"value2\",le=\"+Inf\"} 3\n" + "t_sum{label1=\"value1\",label2=\"value2\"} 125\n" + "t_count{label1=\"value1\",label2=\"value2\"} 3\n" + ); +} diff --git a/unittests/util/prometheus/HttpTests.cpp b/unittests/util/prometheus/HttpTests.cpp index 52665f97..43d4b4bc 100644 --- a/unittests/util/prometheus/HttpTests.cpp +++ b/unittests/util/prometheus/HttpTests.cpp @@ -16,9 +16,11 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ //============================================================================== - #include +#include + +#include #include using namespace util::prometheus; @@ -47,7 +49,8 @@ struct PrometheusCheckRequestTests : public ::testing::TestWithParam req; req.method(GetParam().method); req.target(GetParam().target); @@ -97,11 +100,7 @@ INSTANTIATE_TEST_CASE_P( PrometheusCheckRequestTests::NameGenerator() ); -struct PrometheusHandleRequestTests : ::testing::Test { - PrometheusHandleRequestTests() - { - PrometheusService::init(); - } +struct PrometheusHandleRequestTests : util::prometheus::WithPrometheus { http::request const req{http::verb::get, "/metrics", 11}; }; @@ -116,7 +115,8 @@ TEST_F(PrometheusHandleRequestTests, emptyResponse) TEST_F(PrometheusHandleRequestTests, prometheusDisabled) { - PrometheusService::init(util::Config(boost::json::value{{"prometheus_enabled", false}})); + boost::json::value const configJson({{"prometheus", boost::json::object{{"enabled", false}}}}); + PrometheusService::init(util::Config(configJson)); auto response = handlePrometheusRequest(req, true); ASSERT_TRUE(response.has_value()); EXPECT_EQ(response->result(), http::status::forbidden); @@ -151,7 +151,7 @@ TEST_F(PrometheusHandleRequestTests, responseWithCounter) TEST_F(PrometheusHandleRequestTests, responseWithGauge) { auto const gaugeName = "test_gauge"; - const Labels labels{{{"label2", "value2"}, Label{"label3", "value3"}}}; + const Labels labels{{{"label2", "value2"}, {"label3", "value3"}}}; auto const description = "test_description_gauge"; auto& gauge = PrometheusService::gaugeInt(gaugeName, labels, description); @@ -170,7 +170,7 @@ TEST_F(PrometheusHandleRequestTests, responseWithGauge) TEST_F(PrometheusHandleRequestTests, responseWithCounterAndGauge) { auto const counterName = "test_counter"; - const Labels counterLabels{{{"label1", "value1"}, Label{"label2", "value2"}}}; + const Labels counterLabels{{{"label1", "value1"}, {"label2", "value2"}}}; auto const counterDescription = "test_description"; auto& counter = PrometheusService::counterInt(counterName, counterLabels, counterDescription); @@ -178,7 +178,7 @@ TEST_F(PrometheusHandleRequestTests, responseWithCounterAndGauge) counter += 3; auto const gaugeName = "test_gauge"; - const Labels gaugeLabels{{{"label2", "value2"}, Label{"label3", "value3"}}}; + const Labels gaugeLabels{{{"label2", "value2"}, {"label3", "value3"}}}; auto const gaugeDescription = "test_description_gauge"; auto& gauge = PrometheusService::gaugeInt(gaugeName, gaugeLabels, gaugeDescription); @@ -211,3 +211,19 @@ TEST_F(PrometheusHandleRequestTests, responseWithCounterAndGauge) ); EXPECT_TRUE(response->body() == expectedBody || response->body() == anotherExpectedBody); } + +TEST_F(PrometheusHandleRequestTests, compressReply) +{ + PrometheusService::init(util::Config(boost::json::value{ + {"prometheus", boost::json::object{{"compress_reply", true}}}})); + + auto& gauge = PrometheusService::gaugeInt("test_gauge", Labels{}); + ++gauge; + + 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->operator[](http::field::content_encoding), "gzip"); + EXPECT_GT(response->body().size(), 0ul); +} diff --git a/unittests/util/prometheus/MetricBuilderTests.cpp b/unittests/util/prometheus/MetricBuilderTests.cpp new file mode 100644 index 00000000..04c8ff88 --- /dev/null +++ b/unittests/util/prometheus/MetricBuilderTests.cpp @@ -0,0 +1,78 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include + +#include + +using namespace util::prometheus; + +TEST(MetricBuilderTest, build) +{ + std::string const name = "name"; + std::string const labelsString = "{label1=\"value1\"}"; + MetricBuilder builder; + for (auto const type : + {MetricType::COUNTER_INT, + MetricType::COUNTER_DOUBLE, + MetricType::GAUGE_INT, + MetricType::GAUGE_DOUBLE, + MetricType::HISTOGRAM_INT, + MetricType::HISTOGRAM_DOUBLE}) { + std::unique_ptr metric = [&]() { + if (type == MetricType::HISTOGRAM_INT) + return builder(name, labelsString, type, std::vector{1}); + + if (type == MetricType::HISTOGRAM_DOUBLE) + return builder(name, labelsString, type, std::vector{1.}); + + return builder(name, labelsString, type); + }(); + switch (type) { + case MetricType::COUNTER_INT: + EXPECT_NE(dynamic_cast(metric.get()), nullptr); + break; + case MetricType::COUNTER_DOUBLE: + EXPECT_NE(dynamic_cast(metric.get()), nullptr); + break; + case MetricType::GAUGE_INT: + EXPECT_NE(dynamic_cast(metric.get()), nullptr); + break; + case MetricType::GAUGE_DOUBLE: + EXPECT_NE(dynamic_cast(metric.get()), nullptr); + break; + case MetricType::HISTOGRAM_INT: + EXPECT_NE(dynamic_cast(metric.get()), nullptr); + break; + case MetricType::HISTOGRAM_DOUBLE: + EXPECT_NE(dynamic_cast(metric.get()), nullptr); + break; + default: + EXPECT_EQ(metric, nullptr); + } + if (metric != nullptr) { + EXPECT_EQ(metric->name(), name); + EXPECT_EQ(metric->labelsString(), labelsString); + } + } + EXPECT_DEATH({ builder(name, labelsString, MetricType::SUMMARY, std::vector{}); }, ""); +} diff --git a/unittests/util/prometheus/MetricsTests.cpp b/unittests/util/prometheus/MetricsFamilyTests.cpp similarity index 62% rename from unittests/util/prometheus/MetricsTests.cpp rename to unittests/util/prometheus/MetricsFamilyTests.cpp index 12d9bd21..d1a42614 100644 --- a/unittests/util/prometheus/MetricsTests.cpp +++ b/unittests/util/prometheus/MetricsFamilyTests.cpp @@ -17,66 +17,62 @@ */ //============================================================================== -#include -#include -#include +#include +#include #include #include 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(metric.get()), nullptr); - break; - case MetricType::COUNTER_DOUBLE: - EXPECT_NE(dynamic_cast(metric.get()), nullptr); - break; - case MetricType::GAUGE_INT: - EXPECT_NE(dynamic_cast(metric.get()), nullptr); - break; - case MetricType::GAUGE_DOUBLE: - EXPECT_NE(dynamic_cast(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)); + MOCK_METHOD(void, serializeValue, (OStream&), (const)); }; using MetricStrictMock = ::testing::StrictMock; - struct MetricBuilderImplMock { - MOCK_METHOD(std::unique_ptr, build, (std::string, std::string, MetricType)); + struct MetricBuilderImplMock : MetricBuilderInterface { + std::unique_ptr + operator()( + std::string metricName, + std::string labelsString, + MetricType metricType, + std::vector const& buckets + ) override + { + return buildInt(std::move(metricName), std::move(labelsString), metricType, buckets); + } + + std::unique_ptr + operator()( + std::string metricName, + std::string labelsString, + MetricType metricType, + std::vector const& buckets + ) override + { + return buildDouble(std::move(metricName), std::move(labelsString), metricType, buckets); + } + + MOCK_METHOD( + std::unique_ptr, + buildInt, + (std::string, std::string, MetricType, std::vector const&) + ); + MOCK_METHOD( + std::unique_ptr, + buildDouble, + (std::string, std::string, MetricType, std::vector const&) + ); }; ::testing::StrictMock 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}; + MetricsFamily metricsFamily{name, description, type, metricBuilderMock}; }; TEST_F(MetricsFamilyTest, getters) @@ -90,7 +86,7 @@ TEST_F(MetricsFamilyTest, getMetric) Labels const labels{{{"label1", "value1"}}}; std::string const labelsString = labels.serialize(); - EXPECT_CALL(metricBuilderMock, build(name, labelsString, type)) + EXPECT_CALL(metricBuilderMock, buildInt(name, labelsString, type, std::vector{})) .WillOnce(::testing::Return(std::make_unique(name, labelsString))); auto& metric = metricsFamily.getMetric(labels); @@ -104,7 +100,7 @@ TEST_F(MetricsFamilyTest, getMetric) Labels const labels2{{{"label1", "value2"}}}; std::string const labels2String = labels2.serialize(); - EXPECT_CALL(metricBuilderMock, build(name, labels2String, type)) + EXPECT_CALL(metricBuilderMock, buildInt(name, labels2String, type, std::vector{})) .WillOnce(::testing::Return(std::make_unique(name, labels2String))); auto& metric2 = metricsFamily.getMetric(labels2); @@ -116,11 +112,12 @@ TEST_F(MetricsFamilyTest, getMetric) 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"; }); + EXPECT_CALL(*metricMock, serializeValue(::testing::_)).WillOnce([](OStream& s) { s << "metric"; }); + EXPECT_CALL(*metric2Mock, serializeValue(::testing::_)).WillOnce([](OStream& s) { s << "metric2"; }); - std::string serialized; - metricsFamily.serialize(serialized); + OStream stream{false}; + stream << metricsFamily; + auto const serialized = std::move(stream).data(); auto const expected = fmt::format("# HELP {0} {1}\n# TYPE {0} {2}\nmetric\nmetric2\n\n", name, description, toString(type)); diff --git a/unittests/util/prometheus/OStreamTests.cpp b/unittests/util/prometheus/OStreamTests.cpp new file mode 100644 index 00000000..5931ffb0 --- /dev/null +++ b/unittests/util/prometheus/OStreamTests.cpp @@ -0,0 +1,57 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include +#include + +using namespace util::prometheus; + +TEST(OStreamTests, empty) +{ + OStream stream{false}; + EXPECT_EQ(std::move(stream).data(), ""); +} + +TEST(OStreamTests, string) +{ + OStream stream{false}; + stream << "hello"; + EXPECT_EQ(std::move(stream).data(), "hello"); +} + +TEST(OStreamTests, compression) +{ + OStream stream{true}; + std::string const str = "helloooooooooooooooooooooooooooooooooo"; + stream << str; + auto const compressed = std::move(stream).data(); + EXPECT_LT(compressed.size(), str.size()); + + std::string const decompressed = [&compressed]() { + std::string result; + boost::iostreams::filtering_istream stream; + stream.push(boost::iostreams::gzip_decompressor{}); + stream.push(boost::iostreams::array_source{compressed.data(), compressed.size()}); + stream >> result; + return result; + }(); + EXPECT_EQ(decompressed, str); +} diff --git a/unittests/web/ServerTests.cpp b/unittests/web/ServerTests.cpp index 8fce240f..588c25b8 100644 --- a/unittests/web/ServerTests.cpp +++ b/unittests/web/ServerTests.cpp @@ -583,12 +583,12 @@ TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled) { static auto constexpr JSONServerConfigWithDisabledPrometheus = R"JSON( { - "server":{ + "server": { "ip": "0.0.0.0", "port": 8888, "admin_password": "secret" }, - "prometheus_enabled": false + "prometheus": { "enabled": false } } )JSON";