diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 0a932d37a38c6..d9e8e8ea16605 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -193,6 +193,9 @@ new_features: `. Enabling this option will write a log to all access loggers when HTTP tunnels (e.g. Websocket and CONNECT) are successfully established. +- area: admin + change: | + Adds a new admin stats html bucket-mode ``detailed`` to generate all recorded buckets and summary percentiles. deprecated: - area: access_log diff --git a/docs/root/operations/admin.rst b/docs/root/operations/admin.rst index de2295b581a80..d0957b9061082 100644 --- a/docs/root/operations/admin.rst +++ b/docs/root/operations/admin.rst @@ -475,6 +475,24 @@ modify different aspects of the server: Buckets do not include values from other buckets with smaller upper bounds; the previous bucket's upper bound acts as a lower bound. Compatible with ``usedonly`` and ``filter``. + .. http:get:: /stats?histogram_buckets=detailed + + Shows histograms as both percentile summary data, and raw bucket data. + + Example output + .. code-block:: text + + http.admin.downstream_rq_time: + totals=1,0.25:25, 2,0.25:9 + intervals=1,0.25:2, 2,0.25:3 + summary=P0(1,1) P25(1.0625,1.034) P50(2.0166,1.068) P75(2.058,2.005) P90(2.083,2.06) P95(2.091,2.08) P99(2.09,2.09) P99.5(2.099,2.098) P99.9(2.099,2.099) P100(2.1,2.1) + + Each bucket is shown as `lower_bound,width:count`. In the above example there are two + buckets. `totals` contains the accumulated data-points since the binary was started. + `intervals` shows the new data points since the previous stats flush. + + Compatible with ``usedonly`` and ``filter``. + .. http:get:: /stats?format=html Renders stats using HTML for a web browser, providing form fields to incrementally @@ -647,7 +665,53 @@ modify different aspects of the server: ] } -.. http:get:: /stats?format=prometheus + .. http:get:: /stats?format=json&histogram_buckets=detailed + + Shows histograms as both percentile summary data.. + + Example output: + + .. code-block:: json + + { + "stats": [ + { + "histograms": { + "supported_percentiles": [0, 25, 50, 75, 90, 95, 99, 99.5, 99.9, 100], + "details": [ + { + "name": "http.admin.downstream_rq_time", + "percentiles": [ + { "interval": null, "cumulative": 1 }, + { "interval": null, "cumulative": 1.0351851851851852 }, + { "interval": null "cumulative": 1.0703703703703704 }, + { "interval": null, "cumulative": 2.0136363636363637 }, + { "interval": null "cumulative": 2.0654545454545454 }, + { "interval": null "cumulative": 2.0827272727272725 }, + { "interval": null "cumulative": 2.0965454545454545 }, + { "interval": null, "cumulative": 2.098272727272727 }, + { "interval": null, "cumulative": 2.0996545454545457 }, + { "interval": null "cumulative": 2.1 } + ], + "totals": [ + { "lower_bound": 1, "width": 0.25, "count": 25 }, + { "lower_bound": 2, "width": 0.25, "count": 9 } + ], + "intervals": [ + { "lower_bound": 1, "width": 0.25, "count": 2 }, + { "lower_bound": 2, "width": 0.25, "count": 3 } + ], + }, + ] + } + } + ] + } + + Compatible with ``usedonly`` and ``filter``. + + + .. http:get:: /stats?format=prometheus or alternatively, diff --git a/envoy/stats/histogram.h b/envoy/stats/histogram.h index 6bcb71d77868f..e8aa64cb75329 100644 --- a/envoy/stats/histogram.h +++ b/envoy/stats/histogram.h @@ -167,6 +167,25 @@ class ParentHistogram : public Histogram { * Returns the bucket summary representation. */ virtual std::string bucketSummary() const PURE; + + // Holds detailed value and counts for a histogram bucket. + struct Bucket { + double lower_bound_{0}; // Bound of bucket that's closest to zero. + double width_{0}; + uint64_t count_{0}; + }; + + /** + * @return a vector of histogram buckets collected since binary start or reset. + */ + virtual std::vector detailedTotalBuckets() const PURE; + + /** + * @return bucket data collected since the most recent stat sink. Note that + * the number of interval buckets is likely to be much smaller than + * the number of detailed buckets. + */ + virtual std::vector detailedIntervalBuckets() const PURE; }; using ParentHistogramSharedPtr = RefcountPtr; diff --git a/source/common/stats/histogram_impl.h b/source/common/stats/histogram_impl.h index 23a2adaf23c27..da49db9a436fb 100644 --- a/source/common/stats/histogram_impl.h +++ b/source/common/stats/histogram_impl.h @@ -49,8 +49,6 @@ class HistogramStatisticsImpl final : public HistogramStatistics, NonCopyable { const histogram_t* histogram_ptr, Histogram::Unit unit = Histogram::Unit::Unspecified, ConstSupportedBuckets& supported_buckets = HistogramSettingsImpl::defaultBuckets()); - static ConstSupportedBuckets& defaultSupportedBuckets(); - void refresh(const histogram_t* new_histogram_ptr); // HistogramStatistics diff --git a/source/common/stats/thread_local_store.cc b/source/common/stats/thread_local_store.cc index cc5658d71ae22..aee9013d31d09 100644 --- a/source/common/stats/thread_local_store.cc +++ b/source/common/stats/thread_local_store.cc @@ -958,6 +958,20 @@ std::string ParentHistogramImpl::bucketSummary() const { } } +std::vector +ParentHistogramImpl::detailedlBucketsHelper(const histogram_t& histogram) { + const uint32_t num_buckets = hist_num_buckets(&histogram); + std::vector buckets(num_buckets); + hist_bucket_t hist_bucket; + for (uint32_t i = 0; i < num_buckets; ++i) { + ParentHistogram::Bucket& bucket = buckets[i]; + hist_bucket_idx_bucket(&histogram, i, &hist_bucket, &bucket.count_); + bucket.lower_bound_ = hist_bucket_to_double(hist_bucket); + bucket.width_ = hist_bucket_to_double_bin_width(hist_bucket); + } + return buckets; +} + void ParentHistogramImpl::addTlsHistogram(const TlsHistogramSharedPtr& hist_ptr) { Thread::LockGuard lock(merge_lock_); tls_histograms_.emplace_back(hist_ptr); diff --git a/source/common/stats/thread_local_store.h b/source/common/stats/thread_local_store.h index a205f65295651..46bd0afa1032f 100644 --- a/source/common/stats/thread_local_store.h +++ b/source/common/stats/thread_local_store.h @@ -105,6 +105,12 @@ class ParentHistogramImpl : public MetricImpl { } std::string quantileSummary() const override; std::string bucketSummary() const override; + std::vector detailedTotalBuckets() const override { + return detailedlBucketsHelper(*cumulative_histogram_); + } + std::vector detailedIntervalBuckets() const override { + return detailedlBucketsHelper(*interval_histogram_); + } // Stats::Metric SymbolTable& symbolTable() override; @@ -121,6 +127,8 @@ class ParentHistogramImpl : public MetricImpl { private: bool usedLockHeld() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(merge_lock_); + static std::vector + detailedlBucketsHelper(const histogram_t& histogram); Histogram::Unit unit_; ThreadLocalStoreImpl& thread_local_store_; diff --git a/source/common/stats/utility.h b/source/common/stats/utility.h index 528514d4ec89d..43284f695cec2 100644 --- a/source/common/stats/utility.h +++ b/source/common/stats/utility.h @@ -231,6 +231,7 @@ TextReadout& textReadoutFromElements(Scope& scope, const ElementVec& elements, */ TextReadout& textReadoutFromStatNames(Scope& scope, const StatNameVec& elements, StatNameTagVectorOptConstRef tags = absl::nullopt); + } // namespace Utility /** diff --git a/source/server/admin/stats_handler.cc b/source/server/admin/stats_handler.cc index be645ebddd4f4..fb8dc1801f93b 100644 --- a/source/server/admin/stats_handler.cc +++ b/source/server/admin/stats_handler.cc @@ -186,7 +186,7 @@ Admin::UrlHandler StatsHandler::statsHandler(bool active_mode) { {Admin::ParamDescriptor::Type::Enum, "histogram_buckets", "Histogram bucket display mode", - {"cumulative", "disjoint", "none"}}}; + {"cumulative", "disjoint", "detailed", "none"}}}; Admin::ParamDescriptorVec params; if (!active_mode) { diff --git a/source/server/admin/stats_render.cc b/source/server/admin/stats_render.cc index e75dab1b0a287..7fd55b5b6153e 100644 --- a/source/server/admin/stats_render.cc +++ b/source/server/admin/stats_render.cc @@ -12,9 +12,6 @@ constexpr absl::string_view JsonQuoteCloseBrace = "\"}"; } // namespace namespace Envoy { - -using ProtoMap = Protobuf::Map; - namespace Server { StatsTextRender::StatsTextRender(const StatsParams& params) @@ -32,6 +29,11 @@ void StatsTextRender::generate(Buffer::Instance& response, const std::string& na void StatsTextRender::generate(Buffer::Instance& response, const std::string& name, const Stats::ParentHistogram& histogram) { + if (!histogram.used()) { + response.addFragments({name, ": No recorded values\n"}); + return; + } + switch (histogram_buckets_mode_) { case Utility::HistogramBucketsMode::NoBuckets: response.addFragments({name, ": ", histogram.quantileSummary(), "\n"}); @@ -42,11 +44,28 @@ void StatsTextRender::generate(Buffer::Instance& response, const std::string& na case Utility::HistogramBucketsMode::Disjoint: addDisjointBuckets(name, histogram, response); break; + case Utility::HistogramBucketsMode::Detailed: + response.addFragments({name, ":\n totals="}); + addDetail(histogram.detailedTotalBuckets(), response); + response.add("\n intervals="); + addDetail(histogram.detailedIntervalBuckets(), response); + response.addFragments({"\n summary=", histogram.quantileSummary(), "\n"}); + break; } } void StatsTextRender::finalize(Buffer::Instance&) {} +void StatsTextRender::addDetail(const std::vector& buckets, + Buffer::Instance& response) { + absl::string_view delim = ""; + for (const Stats::ParentHistogram::Bucket& bucket : buckets) { + response.addFragments( + {delim, absl::StrCat(bucket.lower_bound_, ",", bucket.width_, ":", bucket.count_)}); + delim = ", "; + } +} + // Computes disjoint buckets as text and adds them to the response buffer. void StatsTextRender::addDisjointBuckets(const std::string& name, const Stats::ParentHistogram& histogram, @@ -126,9 +145,15 @@ void StatsJsonRender::generate(Buffer::Instance& response, const std::string& na void StatsJsonRender::generate(Buffer::Instance&, const std::string& name, const Stats::ParentHistogram& histogram) { switch (histogram_buckets_mode_) { - case Utility::HistogramBucketsMode::NoBuckets: - summarizeBuckets(name, histogram); + case Utility::HistogramBucketsMode::NoBuckets: { + ProtobufWkt::Struct computed_quantile; + ProtoMap& computed_quantile_fields = *computed_quantile.mutable_fields(); + computed_quantile_fields["name"] = ValueUtil::stringValue(name); + populateQuantiles(histogram, "supported_quantiles", + computed_quantile_fields["values"].mutable_list_value()); + *histogram_array_->add_values() = ValueUtil::structValue(computed_quantile); break; + } case Utility::HistogramBucketsMode::Cumulative: { const Stats::HistogramStatistics& interval_statistics = histogram.intervalStatistics(); const std::vector& interval_buckets = interval_statistics.computedBuckets(); @@ -145,6 +170,32 @@ void StatsJsonRender::generate(Buffer::Instance&, const std::string& name, collectBuckets(name, histogram, interval_buckets, cumulative_buckets); break; } + case Utility::HistogramBucketsMode::Detailed: { + std::vector buckets = histogram.detailedTotalBuckets(); + ProtobufWkt::Struct histogram_obj; + ProtoMap& histogram_obj_fields = *histogram_obj.mutable_fields(); + histogram_obj_fields["name"] = ValueUtil::stringValue(histogram.name()); + populateQuantiles(histogram, "supported_percentiles", + histogram_obj_fields["percentiles"].mutable_list_value()); + populateDetail("totals", histogram.detailedTotalBuckets(), histogram_obj_fields); + populateDetail("intervals", histogram.detailedIntervalBuckets(), histogram_obj_fields); + *histogram_array_->add_values() = ValueUtil::structValue(histogram_obj); + break; + } + } +} + +void StatsJsonRender::populateDetail(absl::string_view name, + const std::vector& buckets, + ProtoMap& histogram_obj_fields) { + ProtobufWkt::ListValue* bucket_array = histogram_obj_fields[name].mutable_list_value(); + for (const Stats::ParentHistogram::Bucket& bucket : buckets) { + ProtobufWkt::Struct bucket_json; + ProtoMap& bucket_fields = *bucket_json.mutable_fields(); + bucket_fields["lower_bound"] = ValueUtil::numberValue(bucket.lower_bound_); + bucket_fields["width"] = ValueUtil::numberValue(bucket.width_); + bucket_fields["count"] = ValueUtil::numberValue(bucket.count_); + *bucket_array->add_values() = ValueUtil::structValue(bucket_json); } } @@ -154,11 +205,18 @@ void StatsJsonRender::finalize(Buffer::Instance& response) { if (histogram_array_->values_size() > 0) { ProtoMap& histograms_obj_container_fields = *histograms_obj_container_.mutable_fields(); if (found_used_histogram_) { - ASSERT(histogram_buckets_mode_ == Utility::HistogramBucketsMode::NoBuckets); - ProtoMap& histograms_obj_fields = *histograms_obj_.mutable_fields(); - histograms_obj_fields["computed_quantiles"].set_allocated_list_value( - histogram_array_.release()); - histograms_obj_container_fields["histograms"] = ValueUtil::structValue(histograms_obj_); + if (histogram_buckets_mode_ == Utility::HistogramBucketsMode::NoBuckets) { + ProtoMap& histograms_obj_fields = *histograms_obj_.mutable_fields(); + histograms_obj_fields["computed_quantiles"].set_allocated_list_value( + histogram_array_.release()); + histograms_obj_container_fields["histograms"] = ValueUtil::structValue(histograms_obj_); + } else if (histogram_buckets_mode_ == Utility::HistogramBucketsMode::Detailed) { + ProtoMap& histograms_obj_fields = *histograms_obj_.mutable_fields(); + histograms_obj_fields["details"].set_allocated_list_value(histogram_array_.release()); + histograms_obj_container_fields["histograms"] = ValueUtil::structValue(histograms_obj_); + } else { + IS_ENVOY_BUG("reached unexpected buckets mode with found_used_histogram_ set"); + } } else { ASSERT(histogram_buckets_mode_ != Utility::HistogramBucketsMode::NoBuckets); histograms_obj_container_fields["histograms"].set_allocated_list_value( @@ -177,38 +235,36 @@ void StatsJsonRender::finalize(Buffer::Instance& response) { response.add("]}"); } +void StatsJsonRender::populateVector(absl::string_view name, const std::vector& values, + uint32_t multiplier, ProtoMap& histograms_obj_fields) { + ProtobufWkt::ListValue* array = histograms_obj_fields[name].mutable_list_value(); + + for (double value : values) { + *array->add_values() = ValueUtil::numberValue(value * multiplier); + } +} + // Summarizes the buckets in the specified histogram, collecting JSON objects. // Note, we do not flush this buffer to the network when it grows large, and // if this becomes an issue it should be possible to do, noting that we are // one or two levels nesting below the list of scalar stats due to the Envoy // stats json schema, where histograms are grouped together. -void StatsJsonRender::summarizeBuckets(const std::string& name, - const Stats::ParentHistogram& histogram) { +void StatsJsonRender::populateQuantiles(const Stats::ParentHistogram& histogram, + absl::string_view label, + ProtobufWkt::ListValue* computed_quantile_value_array) { if (!found_used_histogram_) { // It is not possible for the supported quantiles to differ across histograms, so it is ok // to send them once. Stats::HistogramStatisticsImpl empty_statistics; - ProtoMap& histograms_obj_fields = *histograms_obj_.mutable_fields(); - ProtobufWkt::ListValue* supported_quantile_array = - histograms_obj_fields["supported_quantiles"].mutable_list_value(); - - for (double quantile : empty_statistics.supportedQuantiles()) { - *supported_quantile_array->add_values() = ValueUtil::numberValue(quantile * 100); - } - + ProtoMap& fields = *histograms_obj_.mutable_fields(); + populateVector(label, empty_statistics.supportedQuantiles(), 100 /* multiplier */, fields); found_used_histogram_ = true; } - ProtobufWkt::Struct computed_quantile; - ProtoMap& computed_quantile_fields = *computed_quantile.mutable_fields(); - computed_quantile_fields["name"] = ValueUtil::stringValue(name); - - ProtobufWkt::ListValue* computed_quantile_value_array = - computed_quantile_fields["values"].mutable_list_value(); const Stats::HistogramStatistics& interval_statistics = histogram.intervalStatistics(); const std::vector& computed_quantiles = interval_statistics.computedQuantiles(); - const std::vector& cumulative_quantiles = - histogram.cumulativeStatistics().computedQuantiles(); + const Stats::HistogramStatistics& histogram_stats = histogram.cumulativeStatistics(); + const std::vector& cumulative_quantiles = histogram_stats.computedQuantiles(); const size_t min_size = std::min({computed_quantiles.size(), cumulative_quantiles.size(), interval_statistics.supportedQuantiles().size()}); ASSERT(min_size == computed_quantiles.size()); @@ -226,7 +282,6 @@ void StatsJsonRender::summarizeBuckets(const std::string& name, *computed_quantile_value_array->add_values() = ValueUtil::structValue(computed_quantile_value); } - *histogram_array_->add_values() = ValueUtil::structValue(computed_quantile); } // Collects the buckets from the specified histogram, using either the diff --git a/source/server/admin/stats_render.h b/source/server/admin/stats_render.h index 477c3eb6695e4..baa747e3f4c19 100644 --- a/source/server/admin/stats_render.h +++ b/source/server/admin/stats_render.h @@ -55,6 +55,9 @@ class StatsTextRender : public StatsRender { void addDisjointBuckets(const std::string& name, const Stats::ParentHistogram& histogram, Buffer::Instance& response); + void addDetail(const std::vector& buckets, + Buffer::Instance& response); + const Utility::HistogramBucketsMode histogram_buckets_mode_; }; @@ -73,18 +76,27 @@ class StatsJsonRender : public StatsRender { void finalize(Buffer::Instance& response) override; private: + using ProtoMap = Protobuf::Map; + // Summarizes the buckets in the specified histogram, collecting JSON objects. // Note, we do not flush this buffer to the network when it grows large, and // if this becomes an issue it should be possible to do, noting that we are // one or two levels nesting below the list of scalar stats due to the Envoy // stats json schema, where histograms are grouped together. - void summarizeBuckets(const std::string& name, const Stats::ParentHistogram& histogram); + void populateQuantiles(const Stats::ParentHistogram& histogram, absl::string_view label, + ProtobufWkt::ListValue* computed_quantile_value_array); // Collects the buckets from the specified histogram. void collectBuckets(const std::string& name, const Stats::ParentHistogram& histogram, const std::vector& interval_buckets, const std::vector& cumulative_buckets); + void populateDetail(absl::string_view name, + const std::vector& buckets, + ProtoMap& histogram_obj_fields); + static void populateVector(absl::string_view name, const std::vector& values, + uint32_t multiplier, ProtoMap& histograms_obj_fields); + ProtobufWkt::Struct histograms_obj_; ProtobufWkt::Struct histograms_obj_container_; std::unique_ptr histogram_array_; diff --git a/source/server/admin/utils.cc b/source/server/admin/utils.cc index 774c1395074c7..b348146413c92 100644 --- a/source/server/admin/utils.cc +++ b/source/server/admin/utils.cc @@ -36,6 +36,8 @@ absl::Status histogramBucketsParam(const Http::Utility::QueryParams& params, histogram_buckets_mode = HistogramBucketsMode::Cumulative; } else if (histogram_buckets_query_param.value() == "disjoint") { histogram_buckets_mode = HistogramBucketsMode::Disjoint; + } else if (histogram_buckets_query_param.value() == "detailed") { + histogram_buckets_mode = HistogramBucketsMode::Detailed; } else if (histogram_buckets_query_param.value() != "none") { return absl::InvalidArgumentError( "usage: /stats?histogram_buckets=(cumulative|disjoint|none)\n"); diff --git a/source/server/admin/utils.h b/source/server/admin/utils.h index 6d885f98e3310..5deaf651d8a78 100644 --- a/source/server/admin/utils.h +++ b/source/server/admin/utils.h @@ -13,7 +13,7 @@ namespace Envoy { namespace Server { namespace Utility { -enum class HistogramBucketsMode { NoBuckets, Cumulative, Disjoint }; +enum class HistogramBucketsMode { NoBuckets, Cumulative, Disjoint, Detailed }; void populateFallbackResponseHeaders(Http::Code code, Http::ResponseHeaderMap& header_map); diff --git a/test/common/stats/BUILD b/test/common/stats/BUILD index 36967c7144829..0463996725d8b 100644 --- a/test/common/stats/BUILD +++ b/test/common/stats/BUILD @@ -323,6 +323,7 @@ envoy_cc_test( name = "utility_test", srcs = ["utility_test.cc"], deps = [ + ":stat_test_utility_lib", "//source/common/stats:isolated_store_lib", "//source/common/stats:thread_local_store_lib", "//source/common/stats:utility_lib", diff --git a/test/common/stats/stat_test_utility.cc b/test/common/stats/stat_test_utility.cc index b076a12840e48..1454a8a74a702 100644 --- a/test/common/stats/stat_test_utility.cc +++ b/test/common/stats/stat_test_utility.cc @@ -5,6 +5,17 @@ namespace Envoy { namespace Stats { + +bool operator==(const ParentHistogram::Bucket& a, const ParentHistogram::Bucket& b) { + return a.count_ == b.count_ && std::abs(a.lower_bound_ - b.lower_bound_) < 0.001 && + std::abs(a.width_ - b.width_) < 0.001; +} + +std::ostream& operator<<(std::ostream& out, const ParentHistogram::Bucket& bucket) { + return out << "(min_value=" << bucket.lower_bound_ << ", width=" << bucket.width_ + << ", count=" << bucket.count_ << ")"; +} + namespace TestUtil { void forEachSampleStat(int num_clusters, bool include_other_stats, diff --git a/test/common/stats/stat_test_utility.h b/test/common/stats/stat_test_utility.h index 830f70677ad1d..9112c039754ce 100644 --- a/test/common/stats/stat_test_utility.h +++ b/test/common/stats/stat_test_utility.h @@ -13,6 +13,11 @@ namespace Envoy { namespace Stats { + +// Helper methods to facilitate using testing::ElementsAre with bucket vectors. +bool operator==(const ParentHistogram::Bucket& a, const ParentHistogram::Bucket& b); +std::ostream& operator<<(std::ostream& out, const ParentHistogram::Bucket& bucket); + namespace TestUtil { class TestSymbolTableHelper { diff --git a/test/common/stats/thread_local_store_test.cc b/test/common/stats/thread_local_store_test.cc index 1d43ed663eb64..0363a4cd02195 100644 --- a/test/common/stats/thread_local_store_test.cc +++ b/test/common/stats/thread_local_store_test.cc @@ -37,6 +37,7 @@ using testing::InSequence; using testing::NiceMock; using testing::Ref; using testing::Return; +using testing::UnorderedElementsAre; using testing::UnorderedElementsAreArray; namespace Envoy { @@ -129,6 +130,7 @@ class HistogramWrapper { class HistogramTest : public testing::Test { public: + using Bucket = ParentHistogram::Bucket; using NameHistogramMap = std::map; HistogramTest() @@ -1709,7 +1711,7 @@ TEST_F(HistogramTest, BasicHistogramUsed) { } } -TEST_F(HistogramTest, ParentHistogramBucketSummary) { +TEST_F(HistogramTest, ParentHistogramBucketSummaryAndDetail) { ScopeSharedPtr scope1 = store_->createScope("scope1."); Histogram& histogram = scope_.histogramFromString("histogram", Histogram::Unit::Unspecified); store_->mergeHistograms([]() -> void {}); @@ -1725,6 +1727,8 @@ TEST_F(HistogramTest, ParentHistogramBucketSummary) { "B30000(1,1) B60000(1,1) B300000(1,1) B600000(1,1) B1.8e+06(1,1) " "B3.6e+06(1,1)", parent_histogram->bucketSummary()); + EXPECT_THAT(parent_histogram->detailedTotalBuckets(), UnorderedElementsAre(Bucket{10, 1, 1})); + EXPECT_THAT(parent_histogram->detailedIntervalBuckets(), UnorderedElementsAre(Bucket{10, 1, 1})); } TEST_F(HistogramTest, ForEachHistogram) { diff --git a/test/mocks/stats/mocks.h b/test/mocks/stats/mocks.h index f335594079348..2a145792f50c6 100644 --- a/test/mocks/stats/mocks.h +++ b/test/mocks/stats/mocks.h @@ -200,12 +200,13 @@ class MockParentHistogram : public MockMetric { void merge() override {} std::string quantileSummary() const override { return ""; }; std::string bucketSummary() const override { return ""; }; - MOCK_METHOD(bool, used, (), (const)); MOCK_METHOD(Histogram::Unit, unit, (), (const)); MOCK_METHOD(void, recordValue, (uint64_t value)); MOCK_METHOD(const HistogramStatistics&, cumulativeStatistics, (), (const)); MOCK_METHOD(const HistogramStatistics&, intervalStatistics, (), (const)); + MOCK_METHOD(std::vector, detailedTotalBuckets, (), (const)); + MOCK_METHOD(std::vector, detailedIntervalBuckets, (), (const)); // RefcountInterface void incRefCount() override { refcount_helper_.incRefCount(); } diff --git a/test/server/admin/admin_test.cc b/test/server/admin/admin_test.cc index ec9d0f39d1d97..106f7656e8ef5 100644 --- a/test/server/admin/admin_test.cc +++ b/test/server/admin/admin_test.cc @@ -172,7 +172,7 @@ TEST_P(AdminInstanceTest, Help) { filter: Regular expression (Google re2) for filtering stats format: Format to use; One of (html, active-html, text, json) type: Stat types to include.; One of (All, Counters, Histograms, Gauges, TextReadouts) - histogram_buckets: Histogram bucket display mode; One of (cumulative, disjoint, none) + histogram_buckets: Histogram bucket display mode; One of (cumulative, disjoint, detailed, none) /stats/prometheus: print server stats in prometheus format usedonly: Only include stats that have been written by system since restart text_readouts: Render text_readouts as new gaugues with value 0 (increases Prometheus data size) diff --git a/test/server/admin/stats_render_test.cc b/test/server/admin/stats_render_test.cc index b2f9ce2231531..569995c892be6 100644 --- a/test/server/admin/stats_render_test.cc +++ b/test/server/admin/stats_render_test.cc @@ -47,6 +47,19 @@ TEST_F(StatsRenderTest, TextHistogramDisjoint) { EXPECT_EQ(expected, render<>(renderer, "h1", populateHistogram("h1", {200, 300, 300}))); } +TEST_F(StatsRenderTest, TextHistogramDetailed) { + params_.histogram_buckets_mode_ = Utility::HistogramBucketsMode::Detailed; + StatsTextRender renderer(params_); + constexpr absl::string_view expected = + "h1:\n" + " totals=200,10:1, 300,10:2\n" + " intervals=200,10:1, 300,10:2\n" + " summary=P0(200,200) P25(207.5,207.5) P50(302.5,302.5) P75(306.25,306.25) P90(308.5,308.5) " + "P95(309.25,309.25) P99(309.85,309.85) P99.5(309.925,309.925) P99.9(309.985,309.985) " + "P100(310,310)\n"; + EXPECT_EQ(expected, render<>(renderer, "h1", populateHistogram("h1", {200, 300, 300}))); +} + TEST_F(StatsRenderTest, JsonInt) { StatsJsonRender renderer(response_headers_, response_, params_); const std::string expected = R"EOF({"stats":[ {"value":42, "name":"name"}]})EOF"; @@ -297,5 +310,38 @@ TEST_F(StatsRenderTest, JsonHistogramDisjoint) { JsonStringEq(expected)); } +TEST_F(StatsRenderTest, JsonHistogramDetailed) { + params_.histogram_buckets_mode_ = Utility::HistogramBucketsMode::Detailed; + StatsJsonRender renderer(response_headers_, response_, params_); + const std::string expected = R"EOF( +{ + "stats": [{ + "histograms": { + "supported_percentiles": [0, 25, 50, 75, 90, 95, 99, 99.5, 99.9, 100], + "details": [{ + "name": "h1", + "percentiles": [ + {"cumulative": 200, "interval": 200}, + {"cumulative": 207.5, "interval": 207.5}, + {"cumulative": 302.5, "interval": 302.5}, + {"cumulative": 306.25, "interval": 306.25}, + {"cumulative": 308.5, "interval": 308.5}, + {"cumulative": 309.25, "interval": 309.25}, + {"cumulative": 309.85, "interval": 309.85}, + {"cumulative": 309.925, "interval": 309.925}, + {"cumulative": 309.985, "interval": 309.985}, + {"cumulative": 310, "interval": 310} + ], + "totals": [ + {"lower_bound": 200, "width": 10, "count": 1}, + {"lower_bound": 300, "width": 10, "count": 2}], + "intervals":[ + {"lower_bound": 200, "width": 10, "count": 1}, + {"lower_bound": 300, "width": 10, "count": 2}]}]}}]} + )EOF"; + EXPECT_THAT(render<>(renderer, "h1", populateHistogram("h1", {200, 300, 300})), + JsonStringEq(expected)); +} + } // namespace Server } // namespace Envoy