#include #include "common/stats/thread_local_store.h" #include "server/admin/stats_handler.h" #include "test/server/admin/admin_instance.h" #include "test/test_common/logging.h" #include "test/test_common/utility.h" using testing::EndsWith; using testing::HasSubstr; using testing::InSequence; using testing::Ref; using testing::StartsWith; namespace Envoy { namespace Server { class AdminStatsTest : public testing::TestWithParam { public: AdminStatsTest() : alloc_(symbol_table_) { store_ = std::make_unique(alloc_); store_->addSink(sink_); } static std::string statsAsJsonHandler(std::map& all_stats, std::map& all_text_readouts, const std::vector& all_histograms, const bool used_only, const absl::optional regex = absl::nullopt) { return StatsHandler::statsAsJson(all_stats, all_text_readouts, all_histograms, used_only, regex, true /*pretty_print*/); } Stats::SymbolTableImpl symbol_table_; NiceMock main_thread_dispatcher_; NiceMock tls_; Stats::AllocatorImpl alloc_; Stats::MockSink sink_; Stats::ThreadLocalStoreImplPtr store_; }; INSTANTIATE_TEST_SUITE_P(IpVersions, AdminStatsTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), TestUtility::ipTestParamsToString); TEST_P(AdminStatsTest, StatsAsJson) { InSequence s; store_->initializeThreading(main_thread_dispatcher_, tls_); 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 {}); // Again record a new value in h1 so that it has both interval and cumulative values. // h2 should only have cumulative values. EXPECT_CALL(sink_, onHistogramComplete(Ref(h1), 100)); h1.recordValue(100); store_->mergeHistograms([]() -> void {}); std::vector histograms = store_->histograms(); std::sort(histograms.begin(), histograms.end(), [](const Stats::ParentHistogramSharedPtr& a, const Stats::ParentHistogramSharedPtr& b) -> bool { return a->name() < b->name(); }); std::map all_stats; std::map all_text_readouts; std::string actual_json = statsAsJsonHandler(all_stats, all_text_readouts, histograms, false); const std::string expected_json = R"EOF({ "stats": [ { "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": "h1", "values": [ { "interval": 100.0, "cumulative": 100.0 }, { "interval": 102.5, "cumulative": 105.0 }, { "interval": 105.0, "cumulative": 110.0 }, { "interval": 107.5, "cumulative": 205.0 }, { "interval": 109.0, "cumulative": 208.0 }, { "interval": 109.5, "cumulative": 209.0 }, { "interval": 109.9, "cumulative": 209.8 }, { "interval": 109.95, "cumulative": 209.9 }, { "interval": 109.99, "cumulative": 209.98 }, { "interval": 110.0, "cumulative": 210.0 } ] }, { "name": "h2", "values": [ { "interval": null, "cumulative": 100.0 }, { "interval": null, "cumulative": 102.5 }, { "interval": null, "cumulative": 105.0 }, { "interval": null, "cumulative": 107.5 }, { "interval": null, "cumulative": 109.0 }, { "interval": null, "cumulative": 109.5 }, { "interval": null, "cumulative": 109.9 }, { "interval": null, "cumulative": 109.95 }, { "interval": null, "cumulative": 109.99 }, { "interval": null, "cumulative": 110.0 } ] } ] } } ] })EOF"; EXPECT_THAT(expected_json, JsonStringEq(actual_json)); store_->shutdownThreading(); } TEST_P(AdminStatsTest, UsedOnlyStatsAsJson) { InSequence s; store_->initializeThreading(main_thread_dispatcher_, tls_); Stats::Histogram& h1 = store_->histogramFromString("h1", Stats::Histogram::Unit::Unspecified); Stats::Histogram& h2 = store_->histogramFromString("h2", Stats::Histogram::Unit::Unspecified); EXPECT_EQ("h1", h1.name()); EXPECT_EQ("h2", h2.name()); EXPECT_CALL(sink_, onHistogramComplete(Ref(h1), 200)); h1.recordValue(200); store_->mergeHistograms([]() -> void {}); // Again record a new value in h1 so that it has both interval and cumulative values. // h2 should only have cumulative values. EXPECT_CALL(sink_, onHistogramComplete(Ref(h1), 100)); h1.recordValue(100); store_->mergeHistograms([]() -> void {}); std::map all_stats; std::map all_text_readouts; std::string actual_json = statsAsJsonHandler(all_stats, all_text_readouts, store_->histograms(), true); // Expected JSON should not have h2 values as it is not used. const std::string expected_json = R"EOF({ "stats": [ { "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": "h1", "values": [ { "interval": 100.0, "cumulative": 100.0 }, { "interval": 102.5, "cumulative": 105.0 }, { "interval": 105.0, "cumulative": 110.0 }, { "interval": 107.5, "cumulative": 205.0 }, { "interval": 109.0, "cumulative": 208.0 }, { "interval": 109.5, "cumulative": 209.0 }, { "interval": 109.9, "cumulative": 209.8 }, { "interval": 109.95, "cumulative": 209.9 }, { "interval": 109.99, "cumulative": 209.98 }, { "interval": 110.0, "cumulative": 210.0 } ] } ] } } ] })EOF"; EXPECT_THAT(expected_json, JsonStringEq(actual_json)); store_->shutdownThreading(); } TEST_P(AdminStatsTest, StatsAsJsonFilterString) { InSequence s; store_->initializeThreading(main_thread_dispatcher_, tls_); 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 {}); // Again record a new value in h1 so that it has both interval and cumulative values. // h2 should only have cumulative values. EXPECT_CALL(sink_, onHistogramComplete(Ref(h1), 100)); h1.recordValue(100); store_->mergeHistograms([]() -> void {}); std::map all_stats; std::map all_text_readouts; std::string actual_json = statsAsJsonHandler(all_stats, all_text_readouts, store_->histograms(), false, absl::optional{std::regex("[a-z]1")}); // Because this is a filter case, we don't expect to see any stats except for those containing // "h1" in their name. const std::string expected_json = R"EOF({ "stats": [ { "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": "h1", "values": [ { "interval": 100.0, "cumulative": 100.0 }, { "interval": 102.5, "cumulative": 105.0 }, { "interval": 105.0, "cumulative": 110.0 }, { "interval": 107.5, "cumulative": 205.0 }, { "interval": 109.0, "cumulative": 208.0 }, { "interval": 109.5, "cumulative": 209.0 }, { "interval": 109.9, "cumulative": 209.8 }, { "interval": 109.95, "cumulative": 209.9 }, { "interval": 109.99, "cumulative": 209.98 }, { "interval": 110.0, "cumulative": 210.0 } ] } ] } } ] })EOF"; EXPECT_THAT(expected_json, JsonStringEq(actual_json)); store_->shutdownThreading(); } TEST_P(AdminStatsTest, UsedOnlyStatsAsJsonFilterString) { InSequence s; store_->initializeThreading(main_thread_dispatcher_, tls_); Stats::Histogram& h1 = store_->histogramFromString( "h1_matches", Stats::Histogram::Unit::Unspecified); // Will match, be used, and print Stats::Histogram& h2 = store_->histogramFromString( "h2_matches", Stats::Histogram::Unit::Unspecified); // Will match but not be used Stats::Histogram& h3 = store_->histogramFromString( "h3_not", Stats::Histogram::Unit::Unspecified); // Will be used but not match EXPECT_EQ("h1_matches", h1.name()); EXPECT_EQ("h2_matches", h2.name()); EXPECT_EQ("h3_not", h3.name()); EXPECT_CALL(sink_, onHistogramComplete(Ref(h1), 200)); h1.recordValue(200); EXPECT_CALL(sink_, onHistogramComplete(Ref(h3), 200)); h3.recordValue(200); store_->mergeHistograms([]() -> void {}); // Again record a new value in h1 and h3 so that they have both interval and cumulative values. // h2 should only have cumulative values. EXPECT_CALL(sink_, onHistogramComplete(Ref(h1), 100)); h1.recordValue(100); EXPECT_CALL(sink_, onHistogramComplete(Ref(h3), 100)); h3.recordValue(100); store_->mergeHistograms([]() -> void {}); std::map all_stats; std::map all_text_readouts; std::string actual_json = statsAsJsonHandler(all_stats, all_text_readouts, store_->histograms(), true, absl::optional{std::regex("h[12]")}); // Expected JSON should not have h2 values as it is not used, and should not have h3 values as // they are used but do not match. const std::string expected_json = R"EOF({ "stats": [ { "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": "h1_matches", "values": [ { "interval": 100.0, "cumulative": 100.0 }, { "interval": 102.5, "cumulative": 105.0 }, { "interval": 105.0, "cumulative": 110.0 }, { "interval": 107.5, "cumulative": 205.0 }, { "interval": 109.0, "cumulative": 208.0 }, { "interval": 109.5, "cumulative": 209.0 }, { "interval": 109.9, "cumulative": 209.8 }, { "interval": 109.95, "cumulative": 209.9 }, { "interval": 109.99, "cumulative": 209.98 }, { "interval": 110.0, "cumulative": 210.0 } ] } ] } } ] })EOF"; EXPECT_THAT(expected_json, JsonStringEq(actual_json)); store_->shutdownThreading(); } INSTANTIATE_TEST_SUITE_P(IpVersions, AdminInstanceTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), TestUtility::ipTestParamsToString); TEST_P(AdminInstanceTest, StatsInvalidRegex) { Http::TestResponseHeaderMapImpl header_map; Buffer::OwnedImpl data; EXPECT_LOG_CONTAINS( "error", "Invalid regex: ", EXPECT_EQ(Http::Code::BadRequest, getCallback("/stats?filter=*.test", header_map, data))); // Note: depending on the library, the detailed error message might be one of: // "One of *?+{ was not preceded by a valid regular expression." // "regex_error" // but we always precede by 'Invalid regex: "'. EXPECT_THAT(data.toString(), StartsWith("Invalid regex: \"")); EXPECT_THAT(data.toString(), EndsWith("\"\n")); } TEST_P(AdminInstanceTest, PrometheusStatsInvalidRegex) { Http::TestResponseHeaderMapImpl header_map; Buffer::OwnedImpl data; EXPECT_LOG_CONTAINS( "error", ": *.ptest", EXPECT_EQ(Http::Code::BadRequest, getCallback("/stats?format=prometheus&filter=*.ptest", header_map, data))); // Note: depending on the library, the detailed error message might be one of: // "One of *?+{ was not preceded by a valid regular expression." // "regex_error" // but we always precede by 'Invalid regex: "'. EXPECT_THAT(data.toString(), StartsWith("Invalid regex: \"")); EXPECT_THAT(data.toString(), EndsWith("\"\n")); } TEST_P(AdminInstanceTest, TracingStatsDisabled) { const std::string& name = admin_.tracingStats().service_forced_.name(); for (const Stats::CounterSharedPtr& counter : server_.stats().counters()) { EXPECT_NE(counter->name(), name) << "Unexpected tracing stat found in server stats: " << name; } } TEST_P(AdminInstanceTest, GetRequestJson) { Http::TestResponseHeaderMapImpl response_headers; std::string body; EXPECT_EQ(Http::Code::OK, admin_.request("/stats?format=json", "GET", response_headers, body)); EXPECT_THAT(body, HasSubstr("{\"stats\":[")); EXPECT_THAT(std::string(response_headers.getContentTypeValue()), HasSubstr("application/json")); } TEST_P(AdminInstanceTest, RecentLookups) { Http::TestResponseHeaderMapImpl response_headers; std::string body; // Recent lookup tracking is disabled by default. EXPECT_EQ(Http::Code::OK, admin_.request("/stats/recentlookups", "GET", response_headers, body)); EXPECT_THAT(body, HasSubstr("Lookup tracking is not enabled")); EXPECT_THAT(std::string(response_headers.getContentTypeValue()), HasSubstr("text/plain")); // We can't test RecentLookups in admin unit tests as it doesn't work with a // fake symbol table. However we cover this solidly in integration tests. } } // namespace Server } // namespace Envoy