diff --git a/source/server/admin/stats_handler.cc b/source/server/admin/stats_handler.cc index 0a9bd56257a43..d8426f10abc57 100644 --- a/source/server/admin/stats_handler.cc +++ b/source/server/admin/stats_handler.cc @@ -74,7 +74,6 @@ Http::Code StatsHandler::handlerStats(absl::string_view url, server_.flushStats(); } - Http::Code rc = Http::Code::OK; const Http::Utility::QueryParams params = Http::Utility::parseAndDecodeQueryString(url); const bool used_only = params.find("usedonly") != params.end(); @@ -104,38 +103,27 @@ Http::Code StatsHandler::handlerStats(absl::string_view url, } } - if (const auto format_value = Utility::formatParam(params)) { - 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)); - } else if (format_value.value() == "prometheus") { - return handlerPrometheusStats(url, response_headers, response, admin_stream); - } else { - response.add("usage: /stats?format=json or /stats?format=prometheus \n"); - response.add("\n"); - rc = Http::Code::NotFound; - } - } else { // Display plain stats if format query param is not there. - for (const auto& text_readout : text_readouts) { - response.add(fmt::format("{}: \"{}\"\n", text_readout.first, - Html::Utility::sanitize(text_readout.second))); - } - for (const auto& stat : all_stats) { - response.add(fmt::format("{}: {}\n", stat.first, stat.second)); - } - std::map all_histograms; - for (const Stats::ParentHistogramSharedPtr& histogram : server_.stats().histograms()) { - if (shouldShowMetric(*histogram, used_only, regex)) { - auto insert = all_histograms.emplace(histogram->name(), histogram->quantileSummary()); - ASSERT(insert.second); // No duplicates expected. - } - } - for (const auto& histogram : all_histograms) { - response.add(fmt::format("{}: {}\n", histogram.first, histogram.second)); - } + absl::optional format_value = Utility::formatParam(params); + 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; + } + + 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; + } + + if (format_value.value() == "prometheus") { + return handlerPrometheusStats(url, response_headers, response, admin_stream); } - return rc; + + response.add("usage: /stats?format=json or /stats?format=prometheus \n"); + response.add("\n"); + return Http::Code::NotFound; } Http::Code StatsHandler::handlerPrometheusStats(absl::string_view path_and_query, @@ -174,11 +162,36 @@ 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) { + // Display plain stats if format query param is not there. + for (const auto& text_readout : text_readouts) { + response.add(fmt::format("{}: \"{}\"\n", text_readout.first, + Html::Utility::sanitize(text_readout.second))); + } + for (const auto& stat : all_stats) { + response.add(fmt::format("{}: {}\n", stat.first, stat.second)); + } + std::map all_histograms; + for (const Stats::ParentHistogramSharedPtr& histogram : histograms) { + if (shouldShowMetric(*histogram, used_only, regex)) { + auto insert = all_histograms.emplace(histogram->name(), histogram->quantileSummary()); + ASSERT(insert.second); // No duplicates expected. + } + } + for (const auto& histogram : all_histograms) { + response.add(fmt::format("{}: {}\n", histogram.first, histogram.second)); + } +} + 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 used_only, const absl::optional& regex, const bool pretty_print) { ProtobufWkt::Struct document; diff --git a/source/server/admin/stats_handler.h b/source/server/admin/stats_handler.h index 7934c19c11d1c..84ba69fbde949 100644 --- a/source/server/admin/stats_handler.h +++ b/source/server/admin/stats_handler.h @@ -60,9 +60,14 @@ class StatsHandler : public HandlerContextBase { 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 = absl::nullopt, + 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); }; } // namespace Server diff --git a/test/server/admin/BUILD b/test/server/admin/BUILD index bd564a1540c6c..6f94719122f44 100644 --- a/test/server/admin/BUILD +++ b/test/server/admin/BUILD @@ -61,6 +61,7 @@ envoy_cc_test( ":admin_instance_lib", "//source/common/stats:thread_local_store_lib", "//source/server/admin:stats_handler_lib", + "//test/mocks/server:admin_stream_mocks", "//test/test_common:logging_lib", "//test/test_common:utility_lib", ], diff --git a/test/server/admin/stats_handler_test.cc b/test/server/admin/stats_handler_test.cc index cf1dc13118936..f07c79b8fb467 100644 --- a/test/server/admin/stats_handler_test.cc +++ b/test/server/admin/stats_handler_test.cc @@ -3,6 +3,8 @@ #include "source/common/stats/thread_local_store.h" #include "source/server/admin/stats_handler.h" +#include "test/mocks/server/admin_stream.h" +#include "test/mocks/server/instance.h" #include "test/server/admin/admin_instance.h" #include "test/test_common/logging.h" #include "test/test_common/utility.h" @@ -50,6 +52,188 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, AdminStatsTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), TestUtility::ipTestParamsToString); +TEST_P(AdminStatsTest, HandlerStatsInvalidFormat) { + const std::string url = "/stats?format=blergh"; + Http::TestResponseHeaderMapImpl response_headers; + Buffer::OwnedImpl data; + MockAdminStream admin_stream; + Configuration::MockStatsConfig stats_config; + EXPECT_CALL(stats_config, flushOnAdmin()).WillRepeatedly(testing::Return(false)); + MockInstance instance; + EXPECT_CALL(instance, stats()).WillRepeatedly(testing::ReturnRef(*store_)); + EXPECT_CALL(instance, statsConfig()).WillRepeatedly(testing::ReturnRef(stats_config)); + StatsHandler handler(instance); + Http::Code code = handler.handlerStats(url, response_headers, data, admin_stream); + EXPECT_EQ(Http::Code::NotFound, code); + EXPECT_EQ("usage: /stats?format=json or /stats?format=prometheus \n\n", data.toString()); +} + +TEST_P(AdminStatsTest, HandlerStatsPlainText) { + const std::string url = "/stats"; + Http::TestResponseHeaderMapImpl response_headers; + Buffer::OwnedImpl data; + MockAdminStream admin_stream; + Configuration::MockStatsConfig stats_config; + EXPECT_CALL(stats_config, flushOnAdmin()).WillRepeatedly(testing::Return(false)); + MockInstance instance; + store_->initializeThreading(main_thread_dispatcher_, tls_); + EXPECT_CALL(instance, stats()).WillRepeatedly(testing::ReturnRef(*store_)); + EXPECT_CALL(instance, statsConfig()).WillRepeatedly(testing::ReturnRef(stats_config)); + StatsHandler handler(instance); + + Stats::Counter& c1 = store_->counterFromString("c1"); + Stats::Counter& c2 = store_->counterFromString("c2"); + + c1.add(10); + c2.add(20); + + Stats::TextReadout& t = store_->textReadoutFromString("t"); + t.set("hello world"); + + Stats::Histogram& h1 = store_->histogramFromString("h1", Stats::Histogram::Unit::Unspecified); + Stats::Histogram& h2 = store_->histogramFromString("h2", Stats::Histogram::Unit::Unspecified); + + EXPECT_CALL(sink_, onHistogramComplete(Ref(h1), 200)); + h1.recordValue(200); + + EXPECT_CALL(sink_, onHistogramComplete(Ref(h2), 100)); + h2.recordValue(100); + + store_->mergeHistograms([]() -> void {}); + + Http::Code code = handler.handlerStats(url, response_headers, data, admin_stream); + EXPECT_EQ(Http::Code::OK, code); + EXPECT_EQ("t: \"hello world\"\n" + "c1: 10\n" + "c2: 20\n" + "h1: P0(200.0,200.0) P25(202.5,202.5) P50(205.0,205.0) P75(207.5,207.5) " + "P90(209.0,209.0) P95(209.5,209.5) P99(209.9,209.9) P99.5(209.95,209.95) " + "P99.9(209.99,209.99) P100(210.0,210.0)\n" + "h2: P0(100.0,100.0) P25(102.5,102.5) P50(105.0,105.0) P75(107.5,107.5) " + "P90(109.0,109.0) P95(109.5,109.5) P99(109.9,109.9) P99.5(109.95,109.95) " + "P99.9(109.99,109.99) P100(110.0,110.0)\n", + data.toString()); + + shutdownThreading(); +} + +TEST_P(AdminStatsTest, HandlerStatsJson) { + const std::string url = "/stats?format=json"; + Http::TestResponseHeaderMapImpl response_headers; + Buffer::OwnedImpl data; + MockAdminStream admin_stream; + Configuration::MockStatsConfig stats_config; + EXPECT_CALL(stats_config, flushOnAdmin()).WillRepeatedly(testing::Return(false)); + MockInstance instance; + store_->initializeThreading(main_thread_dispatcher_, tls_); + EXPECT_CALL(instance, stats()).WillRepeatedly(testing::ReturnRef(*store_)); + EXPECT_CALL(instance, statsConfig()).WillRepeatedly(testing::ReturnRef(stats_config)); + StatsHandler handler(instance); + + Stats::Counter& c1 = store_->counterFromString("c1"); + Stats::Counter& c2 = store_->counterFromString("c2"); + + c1.add(10); + c2.add(20); + + Stats::TextReadout& t = store_->textReadoutFromString("t"); + t.set("hello world"); + + Stats::Histogram& h = store_->histogramFromString("h", Stats::Histogram::Unit::Unspecified); + + EXPECT_CALL(sink_, onHistogramComplete(Ref(h), 200)); + h.recordValue(200); + + store_->mergeHistograms([]() -> void {}); + + Http::Code code = handler.handlerStats(url, response_headers, data, admin_stream); + EXPECT_EQ(Http::Code::OK, code); + + const std::string expected_json_old = R"EOF({ + "stats": [ + { + "name":"t", + "value":"hello world" + }, + { + "name":"c1", + "value":10, + }, + { + "name":"c2", + "value":20 + }, + { + "histograms": { + "supported_quantiles": [ + 0.0, + 25.0, + 50.0, + 75.0, + 90.0, + 95.0, + 99.0, + 99.5, + 99.9, + 100.0 + ], + "computed_quantiles": [ + { + "name":"h", + "values": [ + { + "cumulative":200, + "interval":200 + }, + { + "cumulative":202.5, + "interval":202.5 + }, + { + "cumulative":205, + "interval":205 + }, + { + "cumulative":207.5, + "interval":207.5 + }, + { + "cumulative":209, + "interval":209 + }, + { + "cumulative":209.5, + "interval":209.5 + }, + { + "cumulative":209.9, + "interval":209.9 + }, + { + "cumulative":209.95, + "interval":209.95 + }, + { + "cumulative":209.99, + "interval":209.99 + }, + { + "cumulative":210, + "interval":210 + } + ] + }, + ] + } + } + ] +})EOF"; + + EXPECT_THAT(expected_json_old, JsonStringEq(data.toString())); + + shutdownThreading(); +} + TEST_P(AdminStatsTest, StatsAsJson) { InSequence s; store_->initializeThreading(main_thread_dispatcher_, tls_);