From acc53250ea1d8a87c2eed46e6abb9c73c21dbfef Mon Sep 17 00:00:00 2001 From: Joshua Marantz Date: Fri, 21 Jan 2022 11:52:31 -0500 Subject: [PATCH] get basic streaming working for /stats Signed-off-by: Joshua Marantz --- source/server/admin/admin_filter.cc | 20 +++-- source/server/admin/stats_handler.cc | 121 ++++++++++++++++----------- source/server/admin/stats_handler.h | 42 +++++++--- 3 files changed, 117 insertions(+), 66 deletions(-) diff --git a/source/server/admin/admin_filter.cc b/source/server/admin/admin_filter.cc index ecd5fa6e95624..da63489f2a054 100644 --- a/source/server/admin/admin_filter.cc +++ b/source/server/admin/admin_filter.cc @@ -68,14 +68,18 @@ void AdminFilter::onComplete() { Buffer::OwnedImpl response; auto header_map = Http::ResponseHeaderMapImpl::create(); RELEASE_ASSERT(request_headers_, ""); - Http::Code code = admin_server_callback_func_(path, *header_map, response, *this); - Utility::populateFallbackResponseHeaders(code, *header_map); - decoder_callbacks_->encodeHeaders(std::move(header_map), - end_stream_on_complete_ && response.length() == 0, - StreamInfo::ResponseCodeDetails::get().AdminFilterResponse); - - if (response.length() > 0) { - decoder_callbacks_->encodeData(response, end_stream_on_complete_); + for (bool cont = true, first = true; cont; first = false) { + Http::Code code = admin_server_callback_func_(path, *header_map, response, *this); + cont = code == Http::Code::Continue; + if (first) { + Utility::populateFallbackResponseHeaders(cont ? Http::Code::OK : code, *header_map); + decoder_callbacks_->encodeHeaders(std::move(header_map), + end_stream_on_complete_ && response.length() == 0, + StreamInfo::ResponseCodeDetails::get().AdminFilterResponse); + } + if (response.length() > 0) { + decoder_callbacks_->encodeData(response, !cont && end_stream_on_complete_); + } } } diff --git a/source/server/admin/stats_handler.cc b/source/server/admin/stats_handler.cc index 78903b3926905..10bac1359b794 100644 --- a/source/server/admin/stats_handler.cc +++ b/source/server/admin/stats_handler.cc @@ -90,43 +90,65 @@ Http::Code StatsHandler::handlerStats(absl::string_view url, return handlerPrometheusStats(url, response_headers, response, admin_stream); } - std::map all_stats; - for (const Stats::CounterSharedPtr& counter : server_.stats().counters()) { - if (shouldShowMetric(*counter, used_only, regex)) { - all_stats.emplace(counter->name(), counter->value()); + auto iter = context_map_.find(&admin_stream); + if (iter == context_map_.end()) { + auto insertion = context_map_.emplace(&admin_stream, std::make_unique( + server_, used_only, regex, format_value)); + iter = insertion.first; + } + Context& context = *iter->second; + + Http::Code code = Http::Code::NotFound; + if (!format_value.has_value()) { + // Display plain stats if format query param is not there. + code = context.statsAsText(response_headers, response); + } else if (format_value.value() == "json") { + code = context.statsAsJson(response_headers, response, params.find("pretty") != params.end()); + } else { + response.add("usage: /stats?format=json or /stats?format=prometheus \n"); + response.add("\n"); + } + if (code != Http::Code::Continue) { + context_map_.erase(iter); + } + + return code; +} + +StatsHandler::Context::Context(Server::Instance& server, + bool used_only, absl::optional regex, + absl::optional format_value) + : used_only_(used_only), regex_(regex), format_value_(format_value) { + for (const Stats::CounterSharedPtr& counter : server.stats().counters()) { + if (shouldShowMetric(*counter)) { + all_stats_.emplace(counter->name(), counter->value()); } } - for (const Stats::GaugeSharedPtr& gauge : server_.stats().gauges()) { - if (shouldShowMetric(*gauge, used_only, regex)) { + for (const Stats::GaugeSharedPtr& gauge : server.stats().gauges()) { + if (shouldShowMetric(*gauge)) { ASSERT(gauge->importMode() != Stats::Gauge::ImportMode::Uninitialized); - all_stats.emplace(gauge->name(), gauge->value()); + all_stats_.emplace(gauge->name(), gauge->value()); } } + all_stats_iter_ = all_stats_.begin(); - std::map text_readouts; - for (const auto& text_readout : server_.stats().textReadouts()) { - if (shouldShowMetric(*text_readout, used_only, regex)) { - text_readouts.emplace(text_readout->name(), text_readout->value()); + for (const auto& text_readout : server.stats().textReadouts()) { + if (shouldShowMetric(*text_readout)) { + text_readouts_.emplace(text_readout->name(), text_readout->value()); } } + text_readouts_iter_ = text_readouts_.begin(); - if (!format_value.has_value()) { - // Display plain stats if format query param is not there. - statsAsText(all_stats, text_readouts, server_.stats().histograms(), used_only, regex, response); - return Http::Code::OK; - } + histograms_ = server.stats().histograms(); +} - if (format_value.value() == "json") { - response_headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Json); - response.add( - statsAsJson(all_stats, text_readouts, server_.stats().histograms(), used_only, regex)); - return Http::Code::OK; +bool StatsHandler::Context::Context::nextChunk() { + if (++chunk_index_ == chunk_size_) { + chunk_index_= 0; + return true; } - - response.add("usage: /stats?format=json or /stats?format=prometheus \n"); - response.add("\n"); - return Http::Code::NotFound; + return false; } Http::Code StatsHandler::handlerPrometheusStats(absl::string_view path_and_query, @@ -171,22 +193,26 @@ Http::Code StatsHandler::handlerContention(absl::string_view, return Http::Code::OK; } -void StatsHandler::statsAsText(const std::map& all_stats, - const std::map& text_readouts, - const std::vector& histograms, - bool used_only, const absl::optional& regex, - Buffer::Instance& response) { +Http::Code StatsHandler::Context::statsAsText(Http::ResponseHeaderMap& /*response_headers*/, + Buffer::Instance& response) { // Display plain stats if format query param is not there. - for (const auto& text_readout : text_readouts) { + for (; text_readouts_iter_ != text_readouts_.end(); ++text_readouts_iter_) { + if (nextChunk()) { + return Http::Code::Continue; + } response.addFragments( - {text_readout.first, ": \"", Html::Utility::sanitize(text_readout.second), "\"\n"}); + {text_readouts_iter_->first, ": \"", Html::Utility::sanitize(text_readouts_iter_->second), + "\"\n"}); } - for (const auto& stat : all_stats) { - response.addFragments({stat.first, ": ", absl::StrCat(stat.second), "\n"}); + for (; all_stats_iter_ != all_stats_.end(); ++all_stats_iter_) { + if (nextChunk()) { + return Http::Code::Continue; + } + response.addFragments({all_stats_iter_->first, ": ", absl::StrCat(all_stats_iter_->second), "\n"}); } std::map all_histograms; - for (const Stats::ParentHistogramSharedPtr& histogram : histograms) { - if (shouldShowMetric(*histogram, used_only, regex)) { + for (const Stats::ParentHistogramSharedPtr& histogram : histograms_) { + if (shouldShowMetric(*histogram)) { auto insert = all_histograms.emplace(histogram->name(), histogram->quantileSummary()); ASSERT(insert.second); // No duplicates expected. } @@ -194,25 +220,25 @@ void StatsHandler::statsAsText(const std::map& all_stats, for (const auto& histogram : all_histograms) { response.addFragments({histogram.first, ": ", histogram.second, "\n"}); } + return Http::Code::OK; } -std::string -StatsHandler::statsAsJson(const std::map& all_stats, - const std::map& text_readouts, - const std::vector& all_histograms, - const bool used_only, const absl::optional& regex, - const bool pretty_print) { +Http::Code StatsHandler::Context::statsAsJson(Http::ResponseHeaderMap& response_headers, + Buffer::Instance& response, + const bool pretty_print) { + + response_headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Json); ProtobufWkt::Struct document; std::vector stats_array; - for (const auto& text_readout : text_readouts) { + for (const auto& text_readout : text_readouts_) { ProtobufWkt::Struct stat_obj; auto* stat_obj_fields = stat_obj.mutable_fields(); (*stat_obj_fields)["name"] = ValueUtil::stringValue(text_readout.first); (*stat_obj_fields)["value"] = ValueUtil::stringValue(text_readout.second); stats_array.push_back(ValueUtil::structValue(stat_obj)); } - for (const auto& stat : all_stats) { + for (const auto& stat : all_stats_) { ProtobufWkt::Struct stat_obj; auto* stat_obj_fields = stat_obj.mutable_fields(); (*stat_obj_fields)["name"] = ValueUtil::stringValue(stat.first); @@ -228,8 +254,8 @@ StatsHandler::statsAsJson(const std::map& all_stats, std::vector computed_quantile_array; bool found_used_histogram = false; - for (const Stats::ParentHistogramSharedPtr& histogram : all_histograms) { - if (shouldShowMetric(*histogram, used_only, regex)) { + for (const Stats::ParentHistogramSharedPtr& histogram : histograms_) { + if (shouldShowMetric(*histogram)) { if (!found_used_histogram) { // It is not possible for the supported quantiles to differ across histograms, so it is ok // to send them once. @@ -274,7 +300,8 @@ StatsHandler::statsAsJson(const std::map& all_stats, auto* document_fields = document.mutable_fields(); (*document_fields)["stats"] = ValueUtil::listValue(stats_array); - return MessageUtil::getJsonStringFromMessageOrDie(document, pretty_print, true); + response.add(MessageUtil::getJsonStringFromMessageOrDie(document, pretty_print, true)); + return Http::Code::OK; } } // namespace Server diff --git a/source/server/admin/stats_handler.h b/source/server/admin/stats_handler.h index 796bd35bc8ff7..378f12e00494d 100644 --- a/source/server/admin/stats_handler.h +++ b/source/server/admin/stats_handler.h @@ -12,6 +12,7 @@ #include "source/common/stats/histogram_impl.h" #include "source/server/admin/handler_ctx.h" +#include "absl/container/flat_hash_map.h" #include "absl/strings/string_view.h" namespace Envoy { @@ -47,6 +48,35 @@ class StatsHandler : public HandlerContextBase { Http::ResponseHeaderMap& response_headers, Buffer::Instance& response, AdminStream&); + class Context { + public: + Context(Server::Instance& server, + bool used_only, absl::optional regex, + absl::optional format_value); + + bool nextChunk(); + + template bool shouldShowMetric(const StatType& stat) { + return StatsHandler::shouldShowMetric(stat, used_only_, regex_); + } + + Http::Code statsAsText(Http::ResponseHeaderMap& response_headers, Buffer::Instance& response); + Http::Code statsAsJson(Http::ResponseHeaderMap& response_headers, Buffer::Instance& response, + bool pretty); + + const bool used_only_; + absl::optional regex_; + absl::optional format_value_; + std::map all_stats_; + std::map::iterator all_stats_iter_; + std::map text_readouts_; + std::map::iterator text_readouts_iter_; + std::vector histograms_; + static constexpr uint32_t chunk_size_ = 10; + uint32_t chunk_index_{0};; + }; + using ContextPtr = std::unique_ptr; + private: template static bool shouldShowMetric(const StatType& metric, const bool used_only, @@ -57,17 +87,7 @@ class StatsHandler : public HandlerContextBase { friend class StatsHandlerTest; - static std::string statsAsJson(const std::map& all_stats, - const std::map& text_readouts, - const std::vector& all_histograms, - bool used_only, const absl::optional& regex, - bool pretty_print = false); - - void statsAsText(const std::map& all_stats, - const std::map& text_readouts, - const std::vector& all_histograms, - bool used_only, const absl::optional& regex, - Buffer::Instance& response); + absl::flat_hash_map> context_map_; }; } // namespace Server