diff --git a/docs/root/operations/admin.rst b/docs/root/operations/admin.rst index bab1a50409586..cc74f9d8e0c3f 100644 --- a/docs/root/operations/admin.rst +++ b/docs/root/operations/admin.rst @@ -63,7 +63,9 @@ modify different aspects of the server: .. http:get:: / - Render an HTML home page with a table of links to all available options. + Render an HTML home page with a table of links to all available options. This can be + disabled by compiling Envoy with `--define=admin_html=disabled` in which case an error + message is printed. Disabling the HTML mode reduces the Envoy binary size. .. http:get:: /help diff --git a/envoy/server/admin.h b/envoy/server/admin.h index 82f295d36a5c5..496e57544a982 100644 --- a/envoy/server/admin.h +++ b/envoy/server/admin.h @@ -93,6 +93,20 @@ class Admin { public: virtual ~Admin() = default; + // Describes a parameter for an endpoint. This structure is used when + // admin-html has not been disabled to populate an HTML form to enable a + // visitor to the admin console to intuitively specify query-parameters for + // each endpoint. The parameter descriptions also appear in the /help + // endpoint, independent of how Envoy is compiled. + struct ParamDescriptor { + enum class Type { Boolean, String, Enum }; + const Type type_; + const std::string id_; // HTML form ID and query-param name (JS var name rules). + const std::string help_; // Help text rendered into UI. + std::vector enum_choices_{}; + }; + using ParamDescriptorVec = std::vector; + // Represents a request for admin endpoints, enabling streamed responses. class Request { public: @@ -127,6 +141,23 @@ class Admin { }; using RequestPtr = std::unique_ptr; + /** + * Lambda to generate a Request. + */ + using GenRequestFn = std::function; + + /** + * Individual admin handler including prefix, help text, and callback. + */ + struct UrlHandler { + const std::string prefix_; + const std::string help_text_; + const GenRequestFn handler_; + const bool removable_; + const bool mutates_server_state_; + const ParamDescriptorVec params_{}; + }; + /** * Callback for admin URL handlers. * @param path_and_query supplies the path and query of the request URL. @@ -141,11 +172,6 @@ class Admin { absl::string_view path_and_query, Http::ResponseHeaderMap& response_headers, Buffer::Instance& response, AdminStream& admin_stream)>; - /** - * Lambda to generate a Request. - */ - using GenRequestFn = std::function; - /** * Add a legacy admin handler where the entire response is written in * one chunk. @@ -155,10 +181,12 @@ class Admin { * @param callback supplies the callback to invoke when the prefix matches. * @param removable if true allows the handler to be removed via removeHandler. * @param mutates_server_state indicates whether callback will mutate server state. + * @param params command parameter descriptors. * @return bool true if the handler was added, false if it was not added. */ virtual bool addHandler(const std::string& prefix, const std::string& help_text, - HandlerCb callback, bool removable, bool mutates_server_state) PURE; + HandlerCb callback, bool removable, bool mutates_server_state, + const ParamDescriptorVec& params = {}) PURE; /** * Adds a an chunked admin handler. @@ -168,11 +196,13 @@ class Admin { * @param gen_request supplies the callback to generate a Request. * @param removable if true allows the handler to be removed via removeHandler. * @param mutates_server_state indicates whether callback will mutate server state. + * @param params command parameter descriptors. * @return bool true if the handler was added, false if it was not added. */ virtual bool addStreamingHandler(const std::string& prefix, const std::string& help_text, GenRequestFn gen_request, bool removable, - bool mutates_server_state) PURE; + bool mutates_server_state, + const ParamDescriptorVec& params = {}) PURE; /** * Remove an admin handler if it is removable. diff --git a/source/server/admin/BUILD b/source/server/admin/BUILD index bbc3c475f76fe..c9e988f6b8a0b 100644 --- a/source/server/admin/BUILD +++ b/source/server/admin/BUILD @@ -88,6 +88,19 @@ envoy_cc_library( ], ) +genrule( + name = "generate_admin_html", + srcs = [ + "admin_head_start.html", + "admin.css", + ], + outs = ["admin_html_gen.h"], + cmd = "./$(location :generate_admin_html.sh) \ + $(location admin_head_start.html) $(location admin.css) > $@", + tools = [":generate_admin_html.sh"], + visibility = ["//visibility:private"], +) + envoy_cc_library( name = "handler_ctx_lib", hdrs = ["handler_ctx.h"], @@ -103,6 +116,8 @@ envoy_cc_library( deps = [ ":utils_lib", "//envoy/buffer:buffer_interface", + "//envoy/http:codes_interface", + "//envoy/server:admin_interface", "//source/common/stats:histogram_lib", ], ) @@ -114,10 +129,10 @@ envoy_cc_library( deps = [ ":handler_ctx_lib", ":prometheus_stats_lib", + ":stats_params_lib", ":stats_render_lib", ":utils_lib", "//envoy/http:codes_interface", - "//envoy/server:admin_interface", "//source/common/http:codes_lib", "//source/common/http:header_map_lib", "//source/common/stats:histogram_lib", @@ -127,16 +142,23 @@ envoy_cc_library( envoy_cc_library( name = "stats_render_lib", - srcs = ["stats_render.cc"], - hdrs = ["stats_render.h"], + srcs = ["stats_render.cc"] + envoy_select_admin_html([ + ":generate_admin_html", + "stats_html_render.cc", + ]), + hdrs = ["stats_render.h"] + envoy_select_admin_html([ + "stats_html_render.h", + ]), deps = [ ":stats_params_lib", ":utils_lib", + "//envoy/server:admin_interface", "//source/common/buffer:buffer_lib", - "//source/common/html:utility_lib", "//source/common/json:json_sanitizer_lib", "//source/common/stats:histogram_lib", - ], + ] + envoy_select_admin_html([ + "//source/common/html:utility_lib", + ]), ) envoy_cc_library( diff --git a/source/server/admin/admin.cc b/source/server/admin/admin.cc index a821f2f306661..95c254b49b6b8 100644 --- a/source/server/admin/admin.cc +++ b/source/server/admin/admin.cc @@ -99,10 +99,26 @@ AdminImpl::AdminImpl(const std::string& profile_path, Server::Instance& server, MAKE_ADMIN_HANDLER(server_info_handler_.handlerCerts), false, false), makeHandler("/clusters", "upstream cluster status", MAKE_ADMIN_HANDLER(clusters_handler_.handlerClusters), false, false), - makeHandler("/config_dump", "dump current Envoy configs (experimental)", - MAKE_ADMIN_HANDLER(config_dump_handler_.handlerConfigDump), false, false), + makeHandler( + "/config_dump", "dump current Envoy configs (experimental)", + MAKE_ADMIN_HANDLER(config_dump_handler_.handlerConfigDump), false, false, + {{Admin::ParamDescriptor::Type::String, "resource", "The resource to dump"}, + {Admin::ParamDescriptor::Type::String, "mask", + "The mask to apply. When both resource and mask are specified, " + "the mask is applied to every element in the desired repeated field so that only a " + "subset of fields are returned. The mask is parsed as a ProtobufWkt::FieldMask"}, + {Admin::ParamDescriptor::Type::String, "name_regex", + "Dump only the currently loaded configurations whose names match the specified " + "regex. Can be used with both resource and mask query parameters."}, + {Admin::ParamDescriptor::Type::Boolean, "include_eds", + "Dump currently loaded configuration including EDS. See the response definition " + "for more information"}}), makeHandler("/init_dump", "dump current Envoy init manager information (experimental)", - MAKE_ADMIN_HANDLER(init_dump_handler_.handlerInitDump), false, false), + MAKE_ADMIN_HANDLER(init_dump_handler_.handlerInitDump), false, false, + {{Admin::ParamDescriptor::Type::String, "mask", + "The desired component to dump unready targets. The mask is parsed as " + "a ProtobufWkt::FieldMask. For example, get the unready targets of " + "all listeners with /init_dump?mask=listener`"}}), makeHandler("/contention", "dump current Envoy mutex contention stats (if enabled)", MAKE_ADMIN_HANDLER(stats_handler_.handlerContention), false, false), makeHandler("/cpuprofiler", "enable/disable the CPU profiler", @@ -118,8 +134,13 @@ AdminImpl::AdminImpl(const std::string& profile_path, Server::Instance& server, makeHandler("/hot_restart_version", "print the hot restart compatibility version", MAKE_ADMIN_HANDLER(server_info_handler_.handlerHotRestartVersion), false, false), + + // TODO(jmarantz): add support for param-passing through a POST. Browsers send + // those params as the post-body rather than query-params and that requires some + // re-plumbing through the admin callback API. See also drain_listeners. makeHandler("/logging", "query/change logging levels", MAKE_ADMIN_HANDLER(logs_handler_.handlerLogging), false, true), + makeHandler("/memory", "print current allocation/heap usage", MAKE_ADMIN_HANDLER(server_info_handler_.handlerMemory), false, false), makeHandler("/quitquitquit", "exit the server", @@ -132,9 +153,16 @@ AdminImpl::AdminImpl(const std::string& profile_path, Server::Instance& server, MAKE_ADMIN_HANDLER(server_info_handler_.handlerServerInfo), false, false), makeHandler("/ready", "print server state, return 200 if LIVE, otherwise return 503", MAKE_ADMIN_HANDLER(server_info_handler_.handlerReady), false, false), - makeStreamingHandler("/stats", "print server stats", stats_handler_, false, false), + stats_handler_.statsHandler(), makeHandler("/stats/prometheus", "print server stats in prometheus format", - MAKE_ADMIN_HANDLER(stats_handler_.handlerPrometheusStats), false, false), + MAKE_ADMIN_HANDLER(stats_handler_.handlerPrometheusStats), false, false, + {{ParamDescriptor::Type::Boolean, "usedonly", + "Only include stats that have been written by system since restart"}, + {ParamDescriptor::Type::Boolean, "text_readouts", + "Render text_readouts as new gaugues with value 0 (increases Prometheus " + "data size)"}, + {ParamDescriptor::Type::String, "filter", + "Regular expression (ecmascript) for filtering stats"}}), makeHandler("/stats/recentlookups", "Show recent stat-name lookups", MAKE_ADMIN_HANDLER(stats_handler_.handlerStatsRecentLookups), false, false), makeHandler("/stats/recentlookups/clear", "clear list of stat-name lookups and counter", @@ -147,10 +175,19 @@ AdminImpl::AdminImpl(const std::string& profile_path, Server::Instance& server, "/stats/recentlookups/enable", "enable recording of reset stat-name lookup names", MAKE_ADMIN_HANDLER(stats_handler_.handlerStatsRecentLookupsEnable), false, true), makeHandler("/listeners", "print listener info", - MAKE_ADMIN_HANDLER(listeners_handler_.handlerListenerInfo), false, false), + MAKE_ADMIN_HANDLER(listeners_handler_.handlerListenerInfo), false, false, + {{Admin::ParamDescriptor::Type::Enum, + "format", + "File format to use", + {"text", "json"}}}), makeHandler("/runtime", "print runtime values", MAKE_ADMIN_HANDLER(runtime_handler_.handlerRuntime), false, false), - makeHandler("/runtime_modify", "modify runtime values", + makeHandler("/runtime_modify", + "Adds or modifies runtime values as passed in query parameters. To delete a " + "previously added key, use an empty string as the value. Note that deletion " + "only applies to overrides added via this endpoint; values loaded from disk " + "can be modified via override but not deleted. E.g. " + "?key1=value1&key2=value2...", MAKE_ADMIN_HANDLER(runtime_handler_.handlerRuntimeModify), false, true), makeHandler("/reopen_logs", "reopen access logs", MAKE_ADMIN_HANDLER(logs_handler_.handlerReopenLogs), false, true), @@ -158,7 +195,15 @@ AdminImpl::AdminImpl(const std::string& profile_path, Server::Instance& server, date_provider_(server.dispatcher().timeSource()), admin_filter_chain_(std::make_shared()), local_reply_(LocalReply::Factory::createDefault()), - ignore_global_conn_limit_(ignore_global_conn_limit) {} + ignore_global_conn_limit_(ignore_global_conn_limit) { +#ifndef NDEBUG + // Verify that no duplicate handlers exist. + absl::flat_hash_set handlers; + for (const UrlHandler& handler : handlers_) { + ASSERT(handlers.insert(handler.prefix_).second); + } +#endif +} Http::ServerConnectionPtr AdminImpl::createCodec(Network::Connection& connection, const Buffer::Instance& data, @@ -322,6 +367,13 @@ void AdminImpl::getHelp(Buffer::Instance& response) { // Prefix order is used during searching, but for printing do them in alpha order. for (const UrlHandler* handler : sortedHandlers()) { response.add(fmt::format(" {}: {}\n", handler->prefix_, handler->help_text_)); + for (const ParamDescriptor& param : handler->params_) { + response.add(fmt::format(" {}: {}", param.id_, param.help_)); + if (param.type_ == ParamDescriptor::Type::Enum) { + response.addFragments({"; One of (", absl::StrJoin(param.enum_choices_, ", "), ")"}); + } + response.add("\n"); + } } } @@ -331,12 +383,15 @@ const Network::Address::Instance& AdminImpl::localAddress() { AdminImpl::UrlHandler AdminImpl::makeHandler(const std::string& prefix, const std::string& help_text, HandlerCb callback, - bool removable, bool mutates_state) { - return UrlHandler{prefix, help_text, RequestGasket::makeGen(callback), removable, mutates_state}; + bool removable, bool mutates_state, + const ParamDescriptorVec& params) { + return UrlHandler{prefix, help_text, RequestGasket::makeGen(callback), + removable, mutates_state, params}; } bool AdminImpl::addStreamingHandler(const std::string& prefix, const std::string& help_text, - GenRequestFn callback, bool removable, bool mutates_state) { + GenRequestFn callback, bool removable, bool mutates_state, + const ParamDescriptorVec& params) { ASSERT(prefix.size() > 1); ASSERT(prefix[0] == '/'); @@ -353,16 +408,17 @@ bool AdminImpl::addStreamingHandler(const std::string& prefix, const std::string auto it = std::find_if(handlers_.cbegin(), handlers_.cend(), [&prefix](const UrlHandler& entry) { return prefix == entry.prefix_; }); if (it == handlers_.end()) { - handlers_.push_back({prefix, help_text, callback, removable, mutates_state}); + handlers_.push_back({prefix, help_text, callback, removable, mutates_state, params}); return true; } return false; } bool AdminImpl::addHandler(const std::string& prefix, const std::string& help_text, - HandlerCb callback, bool removable, bool mutates_state) { + HandlerCb callback, bool removable, bool mutates_state, + const ParamDescriptorVec& params) { return addStreamingHandler(prefix, help_text, RequestGasket::makeGen(callback), removable, - mutates_state); + mutates_state, params); } bool AdminImpl::removeHandler(const std::string& prefix) { diff --git a/source/server/admin/admin.css b/source/server/admin/admin.css new file mode 100644 index 0000000000000..c9d7698065f03 --- /dev/null +++ b/source/server/admin/admin.css @@ -0,0 +1,42 @@ +.home-table { + font-family: sans-serif; + font-size: medium; + border-collapse: collapse; + border-spacing: 0; +} + +.home-data { + text-align: left; + padding: 4px; +} + +.home-form { + margin-bottom: 0; +} + +.button-as-link { + background: none!important; + border: none; + padding: 0!important; + font-family: sans-serif; + font-size: medium; + color: #069; + text-decoration: underline; + cursor: pointer; +} + +.gray { + background-color: #dddddd; +} + +.vert-space { + height: 4px; +} + +.option { + padding-bottom: 4px; + padding-top: 4px; + padding-right: 4px; + padding-left: 20px; + text-align: right; +} diff --git a/source/server/admin/admin.h b/source/server/admin/admin.h index 887dafbae9b1e..e466f9f5302c6 100644 --- a/source/server/admin/admin.h +++ b/source/server/admin/admin.h @@ -83,10 +83,11 @@ class AdminImpl : public Admin, // // The prefix must start with "/" and contain at least one additional character. bool addHandler(const std::string& prefix, const std::string& help_text, HandlerCb callback, - bool removable, bool mutates_server_state) override; + bool removable, bool mutates_server_state, + const ParamDescriptorVec& params = {}) override; bool addStreamingHandler(const std::string& prefix, const std::string& help_text, - GenRequestFn callback, bool removable, - bool mutates_server_state) override; + GenRequestFn callback, bool removable, bool mutates_server_state, + const ParamDescriptorVec& params = {}) override; bool removeHandler(const std::string& prefix) override; ConfigTracker& getConfigTracker() override; @@ -215,17 +216,6 @@ class AdminImpl : public Admin, private: friend class AdminTestingPeer; - /** - * Individual admin handler including prefix, help text, and callback. - */ - struct UrlHandler { - const std::string prefix_; - const std::string help_text_; - const GenRequestFn handler_; - const bool removable_; - const bool mutates_server_state_; - }; - /** * Creates a Request from a url. */ @@ -235,7 +225,8 @@ class AdminImpl : public Admin, * Creates a UrlHandler structure from a non-chunked callback. */ UrlHandler makeHandler(const std::string& prefix, const std::string& help_text, - HandlerCb callback, bool removable, bool mutates_state); + HandlerCb callback, bool removable, bool mutates_state, + const ParamDescriptorVec& params = {}); /** * Creates a URL prefix bound to chunked handler. Handler is expected to diff --git a/source/server/admin/admin_head_start.html b/source/server/admin/admin_head_start.html new file mode 100644 index 0000000000000..8096871b20c73 --- /dev/null +++ b/source/server/admin/admin_head_start.html @@ -0,0 +1,2 @@ + Envoy Admin + diff --git a/source/server/admin/admin_html.cc b/source/server/admin/admin_html.cc index 3745bf85538d4..05eb41e6cb52d 100644 --- a/source/server/admin/admin_html.cc +++ b/source/server/admin/admin_html.cc @@ -1,120 +1,24 @@ #include "source/common/html/utility.h" #include "source/server/admin/admin.h" +#include "source/server/admin/stats_html_render.h" namespace Envoy { namespace Server { -namespace { - -/** - * Favicon base64 image was harvested by screen-capturing the favicon from a Chrome tab - * while visiting https://www.envoyproxy.io/. The resulting PNG was translated to base64 - * by dropping it into https://www.base64-image.de/ and then pasting the resulting string - * below. - * - * The actual favicon source for that, https://www.envoyproxy.io/img/favicon.ico is nicer - * because it's transparent, but is also 67646 bytes, which is annoying to inline. We could - * just reference that rather than inlining it, but then the favicon won't work when visiting - * the admin page from a network that can't see the internet. - */ -const char EnvoyFavicon[] = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1" - "BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAH9SURBVEhL7ZRdTttAFIUrUFaAX5w9gIhgUfzshFRK+gIbaVbA" - "zwaqCly1dSpKk5A485/YCdXpHTB4BsdgVe0bD0cZ3Xsm38yZ8byTUuJ/6g3wqqoBrBhPTzmmLfptMbAzttJTpTKAF2MWC" - "7ADCdNIwXZpvMMwayiIwwS874CcOc9VuQPR1dBBChPMITpFXXU45hukIIH6kHhzVqkEYB8F5HYGvZ5B7EvwmHt9K/59Cr" - "U3QbY2RNYaQPYmJc+jPIBICNCcg20ZsAsCPfbcrFlRF+cJZpvXSJt9yMTxO/IAzJrCOfhJXiOgFEX/SbZmezTWxyNk4Q9" - "anHMmjnzAhEyhAW8LCE6wl26J7ZFHH1FMYQxh567weQBOO1AW8D7P/UXAQySq/QvL8Fu9HfCEw4SKALm5BkC3bwjwhSKr" - "A5hYAMXTJnPNiMyRBVzVjcgCyHiSm+8P+WGlnmwtP2RzbCMiQJ0d2KtmmmPorRHEhfMROVfTG5/fYrF5iWXzE80tfy9WP" - "sCqx5Buj7FYH0LvDyHiqd+3otpsr4/fa5+xbEVQPfrYnntylQG5VGeMLBhgEfyE7o6e6qYzwHIjwl0QwXSvvTmrVAY4D5" - "ddvT64wV0jRrr7FekO/XEjwuwwhuw7Ef7NY+dlfXpLb06EtHUJdVbsxvNUqBrwj/QGeEUSfwBAkmWHn5Bb/gAAAABJRU5"; - -const char AdminHtmlStart[] = R"( - - Envoy Admin - - - - - - - - - - -)"; - -const char AdminHtmlEnd[] = R"( - -
CommandDescription
- -)"; - -} // namespace - Http::Code AdminImpl::handlerAdminHome(absl::string_view, Http::ResponseHeaderMap& response_headers, Buffer::Instance& response, AdminStream&) { - response_headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Html); - - response.add(absl::StrReplaceAll(AdminHtmlStart, {{"@FAVICON@", EnvoyFavicon}})); + StatsHtmlRender html(response_headers, response, StatsParams()); + html.tableBegin(response); // Prefix order is used during searching, but for printing do them in alpha order. + OptRef no_query_params; for (const UrlHandler* handler : sortedHandlers()) { - absl::string_view path = handler->prefix_; - - if (path == "/") { - continue; // No need to print self-link to index page. - } - - // Remove the leading slash from the link, so that the admin page can be - // rendered as part of another console, on a sub-path. - // - // E.g. consider a downstream dashboard that embeds the Envoy admin console. - // In that case, the "/stats" endpoint would be at - // https://DASHBOARD/envoy_admin/stats. If the links we present on the home - // page are absolute (e.g. "/stats") they won't work in the context of the - // dashboard. Removing the leading slash, they will work properly in both - // the raw admin console and when embedded in another page and URL - // hierarchy. - ASSERT(!path.empty()); - ASSERT(path[0] == '/'); - path = path.substr(1); + html.urlHandler(response, *handler, no_query_params); + } - // For handlers that mutate state, render the link as a button in a POST form, - // rather than an anchor tag. This should discourage crawlers that find the / - // page from accidentally mutating all the server state by GETting all the hrefs. - const char* link_format = - handler->mutates_server_state_ - ? "
" - : "{}"; - const std::string link = fmt::format(link_format, path, path); + html.tableEnd(response); + html.finalize(response); - // Handlers are all specified by statically above, and are thus trusted and do - // not require escaping. - response.add(fmt::format("{}" - "{}\n", - link, Html::Utility::sanitize(handler->help_text_))); - } - response.add(AdminHtmlEnd); return Http::Code::OK; } diff --git a/source/server/admin/admin_no_html.cc b/source/server/admin/admin_no_html.cc index 5d420809daf50..9335cdfce4701 100644 --- a/source/server/admin/admin_no_html.cc +++ b/source/server/admin/admin_no_html.cc @@ -1,4 +1,3 @@ -#include "source/common/html/utility.h" #include "source/server/admin/admin.h" namespace Envoy { diff --git a/source/server/admin/generate_admin_html.sh b/source/server/admin/generate_admin_html.sh new file mode 100755 index 0000000000000..c67783831e4ea --- /dev/null +++ b/source/server/admin/generate_admin_html.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +echo '#include "absl/strings/str_replace.h"' +echo '' +echo 'constexpr absl::string_view AdminHtmlStart = R"EOF(' +echo '' +cat "$1" +echo '' +echo '' +echo ')EOF";' diff --git a/source/server/admin/stats_handler.cc b/source/server/admin/stats_handler.cc index 15f6613fb26e1..f8ad9e810166b 100644 --- a/source/server/admin/stats_handler.cc +++ b/source/server/admin/stats_handler.cc @@ -4,6 +4,7 @@ #include #include "envoy/admin/v3/mutex_stats.pb.h" +#include "envoy/server/admin.h" #include "source/common/buffer/buffer_impl.h" #include "source/common/common/empty_string.h" @@ -12,6 +13,8 @@ #include "source/server/admin/prometheus_stats.h" #include "source/server/admin/stats_request.h" +#include "absl/strings/numbers.h" + namespace Envoy { namespace Server { @@ -95,11 +98,13 @@ Admin::RequestPtr StatsHandler::makeRequest(absl::string_view path, AdminStream& server_.flushStats(); } - return makeRequest(server_.stats(), params); + return makeRequest(server_.stats(), params, + [this]() -> Admin::UrlHandler { return statsHandler(); }); } -Admin::RequestPtr StatsHandler::makeRequest(Stats::Store& stats, const StatsParams& params) { - return std::make_unique(stats, params); +Admin::RequestPtr StatsHandler::makeRequest(Stats::Store& stats, const StatsParams& params, + StatsRequest::UrlHandlerFn url_handler_fn) { + return std::make_unique(stats, params, url_handler_fn); } Http::Code StatsHandler::handlerPrometheusStats(absl::string_view path_and_query, @@ -115,6 +120,11 @@ Http::Code StatsHandler::prometheusStats(absl::string_view path_and_query, if (code != Http::Code::OK) { return code; } + + if (server_.statsConfig().flushOnAdmin()) { + server_.flushStats(); + } + prometheusFlushAndRender(params, response); return Http::Code::OK; } @@ -156,5 +166,30 @@ Http::Code StatsHandler::handlerContention(absl::string_view, return Http::Code::OK; } +Admin::UrlHandler StatsHandler::statsHandler() { + return { + "/stats", + "print server stats", + [this](absl::string_view path, AdminStream& admin_stream) -> Admin::RequestPtr { + return makeRequest(path, admin_stream); + }, + false, + false, + {{Admin::ParamDescriptor::Type::Boolean, "usedonly", + "Only include stats that have been written by system since restart"}, + {Admin::ParamDescriptor::Type::String, "filter", + "Regular expression (ecmascript) for filtering stats"}, + {Admin::ParamDescriptor::Type::Enum, "format", "Format to use", {"html", "text", "json"}}, + {Admin::ParamDescriptor::Type::Enum, + "type", + "Stat types to include.", + {StatLabels::All, StatLabels::Counters, StatLabels::Histograms, StatLabels::Gauges, + StatLabels::TextReadouts}}, + {Admin::ParamDescriptor::Type::Enum, + "histogram_buckets", + "Histogram bucket display mode", + {"cumulative", "disjoint", "none"}}}}; +} + } // namespace Server } // namespace Envoy diff --git a/source/server/admin/stats_handler.h b/source/server/admin/stats_handler.h index 663c62b79cf97..29b7a08fd7a02 100644 --- a/source/server/admin/stats_handler.h +++ b/source/server/admin/stats_handler.h @@ -78,11 +78,28 @@ class StatsHandler : public HandlerContextBase { Http::Code handlerContention(absl::string_view path_and_query, Http::ResponseHeaderMap& response_headers, Buffer::Instance& response, AdminStream&); - Admin::RequestPtr makeRequest(absl::string_view path, AdminStream& admin_stream); - static Admin::RequestPtr makeRequest(Stats::Store& stats, const StatsParams& params); + + /** + * When stats are rendered in HTML mode, we want users to be able to tweak + * parameters after the stats page is rendered, such as tweaking the filter or + * `usedonly`. We use the same stats UrlHandler both for the admin home page + * and for rendering in /stats?format=html. We share the same UrlHandler in + * both contexts by defining an API for it here. + * + * @return a URL handler for stats. + */ + Admin::UrlHandler statsHandler(); + + static Admin::RequestPtr makeRequest(Stats::Store& stats, const StatsParams& params, + StatsRequest::UrlHandlerFn url_handler_fn = nullptr); + Admin::RequestPtr makeRequest(absl::string_view path, AdminStream&); + // static Admin::RequestPtr makeRequest(Stats::Store& stats, const StatsParams& params, + // StatsRequest::UrlHandlerFn url_handler_fn); private: - friend class StatsHandlerTest; + static Http::Code prometheusStats(absl::string_view path_and_query, Buffer::Instance& response, + Stats::Store& stats, + Stats::CustomStatNamespaces& custom_namespaces); }; } // namespace Server diff --git a/source/server/admin/stats_html_render.cc b/source/server/admin/stats_html_render.cc new file mode 100644 index 0000000000000..9615442cfbeb7 --- /dev/null +++ b/source/server/admin/stats_html_render.cc @@ -0,0 +1,207 @@ +#include "source/server/admin/stats_html_render.h" + +#include + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/assert.h" +#include "source/common/html/utility.h" +#include "source/server/admin/admin_html_gen.h" + +#include "absl/strings/str_replace.h" + +namespace { + +/** + * Favicon base64 image was harvested by screen-capturing the favicon from a Chrome tab + * while visiting https://www.envoyproxy.io/. The resulting PNG was translated to base64 + * by dropping it into https://www.base64-image.de/ and then pasting the resulting string + * below. + * + * The actual favicon source for that, https://www.envoyproxy.io/img/favicon.ico is nicer + * because it's transparent, but is also 67646 bytes, which is annoying to inline. We could + * just reference that rather than inlining it, but then the favicon won't work when visiting + * the admin page from a network that can't see the internet. + */ +const char EnvoyFavicon[] = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1" + "BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAH9SURBVEhL7ZRdTttAFIUrUFaAX5w9gIhgUfzshFRK+gIbaVbA" + "zwaqCly1dSpKk5A485/YCdXpHTB4BsdgVe0bD0cZ3Xsm38yZ8byTUuJ/6g3wqqoBrBhPTzmmLfptMbAzttJTpTKAF2MWC" + "7ADCdNIwXZpvMMwayiIwwS874CcOc9VuQPR1dBBChPMITpFXXU45hukIIH6kHhzVqkEYB8F5HYGvZ5B7EvwmHt9K/59Cr" + "U3QbY2RNYaQPYmJc+jPIBICNCcg20ZsAsCPfbcrFlRF+cJZpvXSJt9yMTxO/IAzJrCOfhJXiOgFEX/SbZmezTWxyNk4Q9" + "anHMmjnzAhEyhAW8LCE6wl26J7ZFHH1FMYQxh567weQBOO1AW8D7P/UXAQySq/QvL8Fu9HfCEw4SKALm5BkC3bwjwhSKr" + "A5hYAMXTJnPNiMyRBVzVjcgCyHiSm+8P+WGlnmwtP2RzbCMiQJ0d2KtmmmPorRHEhfMROVfTG5/fYrF5iWXzE80tfy9WP" + "sCqx5Buj7FYH0LvDyHiqd+3otpsr4/fa5+xbEVQPfrYnntylQG5VGeMLBhgEfyE7o6e6qYzwHIjwl0QwXSvvTmrVAY4D5" + "ddvT64wV0jRrr7FekO/XEjwuwwhuw7Ef7NY+dlfXpLb06EtHUJdVbsxvNUqBrwj/QGeEUSfwBAkmWHn5Bb/gAAAABJRU5"; + +const char AdminHtmlTableBegin[] = R"( + + + + + + + + +)"; + +const char AdminHtmlTableEnd[] = R"( + +
CommandDescription
+)"; + +} // namespace + +namespace Envoy { +namespace Server { + +StatsHtmlRender::StatsHtmlRender(Http::ResponseHeaderMap& response_headers, + Buffer::Instance& response, const StatsParams& params) + : StatsTextRender(params) { + response_headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Html); + response.add("\n"); + response.add("\n"); + response.add(absl::StrReplaceAll(AdminHtmlStart, {{"@FAVICON@", EnvoyFavicon}})); + response.add("\n"); +} + +void StatsHtmlRender::finalize(Buffer::Instance& response) { + ASSERT(!finalized_); + finalized_ = true; + if (has_pre_) { + response.add("\n"); + } + response.add("\n"); + response.add(""); +} + +void StatsHtmlRender::startPre(Buffer::Instance& response) { + has_pre_ = true; + response.add("
\n");
+}
+
+void StatsHtmlRender::generate(Buffer::Instance& response, const std::string& name,
+                               const std::string& value) {
+  response.addFragments({name, ": \"", Html::Utility::sanitize(value), "\"\n"});
+}
+
+void StatsHtmlRender::noStats(Buffer::Instance& response, absl::string_view types) {
+  response.addFragments({"
\n
No ", types, " found
\n
\n"});
+}
+
+void StatsHtmlRender::tableBegin(Buffer::Instance& response) { response.add(AdminHtmlTableBegin); }
+
+void StatsHtmlRender::tableEnd(Buffer::Instance& response) { response.add(AdminHtmlTableEnd); }
+
+void StatsHtmlRender::urlHandler(Buffer::Instance& response, const Admin::UrlHandler& handler,
+                                 OptRef query) {
+  absl::string_view path = handler.prefix_;
+
+  if (path == "/") {
+    return; // No need to print self-link to index page.
+  }
+
+  // Remove the leading slash from the link, so that the admin page can be
+  // rendered as part of another console, on a sub-path.
+  //
+  // E.g. consider a downstream dashboard that embeds the Envoy admin console.
+  // In that case, the "/stats" endpoint would be at
+  // https://DASHBOARD/envoy_admin/stats. If the links we present on the home
+  // page are absolute (e.g. "/stats") they won't work in the context of the
+  // dashboard. Removing the leading slash, they will work properly in both
+  // the raw admin console and when embedded in another page and URL
+  // hierarchy.
+  ASSERT(!path.empty());
+  ASSERT(path[0] == '/');
+  std::string sanitized_path = Html::Utility::sanitize(path.substr(1));
+  path = sanitized_path;
+
+  // Alternate gray and white param-blocks. The pure CSS way of coloring based
+  // on row index doesn't work correctly for us as we are using a row for each
+  // parameter, and we want each endpoint/option-block to be colored the same.
+  const char* row_class = (++index_ & 1) ? " class='gray'" : "";
+
+  // For handlers that mutate state, render the link as a button in a POST form,
+  // rather than an anchor tag. This should discourage crawlers that find the /
+  // page from accidentally mutating all the server state by GETting all the hrefs.
+  const char* method = handler.mutates_server_state_ ? "post" : "get";
+  if (submit_on_change_) {
+    response.addFragments({"\n
\n"}); + } else { + response.addFragments({"\n\n\n "}); + if (!handler.mutates_server_state_ && handler.params_.empty()) { + // GET requests without parameters can be simple links rather than forms with + // buttons that are rendered as links. This simplification improves the + // usability of the page with screen-readers. + response.addFragments({"", path, ""}); + } else { + // Render an explicit visible submit as a link (for GET) or button (for POST). + const char* button_style = handler.mutates_server_state_ ? "" : " class='button-as-link'"; + response.addFragments({"
\n ", path, + "\n "}); + } + response.addFragments({"\n ", + Html::Utility::sanitize(handler.help_text_), "\n\n"}); + } + + for (const Admin::ParamDescriptor& param : handler.params_) { + std::string id = absl::StrCat("param-", index_, "-", absl::StrReplaceAll(path, {{"/", "-"}}), + "-", param.id_); + response.addFragments({"\n "}); + input(response, id, param.id_, path, param.type_, query, param.enum_choices_); + response.addFragments({"\n ", "\n\n"}); + } +} + +void StatsHtmlRender::input(Buffer::Instance& response, absl::string_view id, + absl::string_view name, absl::string_view path, + Admin::ParamDescriptor::Type type, + OptRef query, + const std::vector& enum_choices) { + std::string value; + if (query.has_value()) { + auto iter = query->find(std::string(name)); + if (iter != query->end()) { + value = iter->second; + } + } + + std::string on_change; + if (submit_on_change_) { + on_change = absl::StrCat(" onchange='", path, ".submit()'"); + } + + switch (type) { + case Admin::ParamDescriptor::Type::Boolean: + response.addFragments({"" : " checked/>"}); + break; + case Admin::ParamDescriptor::Type::String: { + std::string sanitized; + if (!value.empty()) { + sanitized = absl::StrCat(" value=", Html::Utility::sanitize(value)); + } + response.addFragments({""}); + break; + } + case Admin::ParamDescriptor::Type::Enum: + response.addFragments( + {"\n \n "); + break; + } +} + +} // namespace Server +} // namespace Envoy diff --git a/source/server/admin/stats_html_render.h b/source/server/admin/stats_html_render.h new file mode 100644 index 0000000000000..95e4504d8e382 --- /dev/null +++ b/source/server/admin/stats_html_render.h @@ -0,0 +1,78 @@ +#pragma once + +#include "source/server/admin/stats_render.h" + +namespace Envoy { +namespace Server { + +class StatsHtmlRender : public StatsTextRender { +public: + StatsHtmlRender(Http::ResponseHeaderMap& response_headers, Buffer::Instance& response, + const StatsParams& params); + + void noStats(Buffer::Instance&, absl::string_view types) override; + void generate(Buffer::Instance& response, const std::string& name, + const std::string& value) override; + void generate(Buffer::Instance& response, const std::string& name, uint64_t value) override { + StatsTextRender::generate(response, name, value); + } + void generate(Buffer::Instance& response, const std::string& name, + const Stats::ParentHistogram& histogram) override { + StatsTextRender::generate(response, name, histogram); + } + void finalize(Buffer::Instance&) override; + + /** + * Renders the beginning of the help-table into the response buffer provided + * in the constructor. + */ + void tableBegin(Buffer::Instance&); + + /** + * Renders the end of the help-table into the response buffer provided in the + * constructor. + */ + void tableEnd(Buffer::Instance&); + + /** + * Initiates an HTML PRE section. The PRE will be auto-closed when the render + * object is finalized. + */ + void startPre(Buffer::Instance&); + + /** + * Renders a table row for a URL endpoint, including the name of the endpoint, + * entries for each parameter, and help text. + * + * This must be called after renderTableBegin and before renderTableEnd. Any + * number of URL Handlers can be rendered. + * + * @param handler the URL handler. + */ + void urlHandler(Buffer::Instance&, const Admin::UrlHandler& handler, + OptRef query); + + void input(Buffer::Instance&, absl::string_view id, absl::string_view name, + absl::string_view path, Admin::ParamDescriptor::Type type, + OptRef query, + const std::vector& enum_choices); + + // By default, editing parameters does not cause a form-submit -- you have + // to click on the link or button first. This is useful for the admin home + // page which lays out all the parameters so users can tweak them before submitting. + // + // Calling setSubmitOnChange(true) makes the form auto-submits when any + // parameters change, and does not have its own explicit submit button. This + // is used to enable the user to adjust query-parameters while visiting an + // html-rendered endpoint. + void setSubmitOnChange(bool submit_on_change) { submit_on_change_ = submit_on_change; } + +private: + int index_{0}; // Used to alternate row-group background color + bool submit_on_change_{false}; + bool has_pre_{false}; + bool finalized_{false}; +}; + +} // namespace Server +} // namespace Envoy diff --git a/source/server/admin/stats_params.cc b/source/server/admin/stats_params.cc index 5fe2e20cf6b5e..5fc32eaca399f 100644 --- a/source/server/admin/stats_params.cc +++ b/source/server/admin/stats_params.cc @@ -36,6 +36,29 @@ Http::Code StatsParams::parse(absl::string_view url, Buffer::Instance& response) return Http::Code::BadRequest; } + auto parse_type = [](absl::string_view str, StatsType& type) { + if (str == StatLabels::Gauges) { + type = StatsType::Gauges; + } else if (str == StatLabels::Counters) { + type = StatsType::Counters; + } else if (str == StatLabels::Histograms) { + type = StatsType::Histograms; + } else if (str == StatLabels::TextReadouts) { + type = StatsType::TextReadouts; + } else if (str == StatLabels::All) { + type = StatsType::All; + } else { + return false; + } + return true; + }; + + auto type_iter = query_.find("type"); + if (type_iter != query_.end() && !parse_type(type_iter->second, type_)) { + response.add("invalid &type= param"); + return Http::Code::BadRequest; + } + const absl::optional format_value = Utility::formatParam(query_); if (format_value.has_value()) { if (format_value.value() == "prometheus") { @@ -44,8 +67,15 @@ Http::Code StatsParams::parse(absl::string_view url, Buffer::Instance& response) format_ = StatsFormat::Json; } else if (format_value.value() == "text") { format_ = StatsFormat::Text; + } else if (format_value.value() == "html") { +#ifdef ENVOY_ADMIN_HTML + format_ = StatsFormat::Html; +#else + response.add("HTML output was disabled by building with --define=admin_html=disabled"); + return Http::Code::BadRequest; +#endif } else { - response.add("usage: /stats?format=(json|prometheus|text)\n\n"); + response.add("usage: /stats?format=(html|json|prometheus|text)\n\n"); return Http::Code::BadRequest; } } @@ -53,5 +83,27 @@ Http::Code StatsParams::parse(absl::string_view url, Buffer::Instance& response) return Http::Code::OK; } +absl::string_view StatsParams::typeToString(StatsType type) { + absl::string_view ret; + switch (type) { + case StatsType::TextReadouts: + ret = StatLabels::TextReadouts; + break; + case StatsType::Counters: + ret = StatLabels::Counters; + break; + case StatsType::Gauges: + ret = StatLabels::Gauges; + break; + case StatsType::Histograms: + ret = StatLabels::Histograms; + break; + case StatsType::All: + ret = StatLabels::All; + break; + } + return ret; +} + } // namespace Server } // namespace Envoy diff --git a/source/server/admin/stats_params.h b/source/server/admin/stats_params.h index 02fe51cffac64..f4f0973129cc2 100644 --- a/source/server/admin/stats_params.h +++ b/source/server/admin/stats_params.h @@ -14,12 +14,32 @@ namespace Envoy { namespace Server { +namespace StatLabels { +constexpr absl::string_view All = "All"; +constexpr absl::string_view Counters = "Counters"; +constexpr absl::string_view Gauges = "Gauges"; +constexpr absl::string_view Histograms = "Histograms"; +constexpr absl::string_view TextReadouts = "TextReadouts"; +} // namespace StatLabels + enum class StatsFormat { +#ifdef ENVOY_ADMIN_HTML + Html, +#endif Json, Prometheus, Text, }; +// The order is used to linearize the ordering of stats of all types. +enum class StatsType { + TextReadouts, + Counters, + Gauges, + Histograms, + All, +}; + struct StatsParams { /** * Parses the URL's query parameter, populating this. @@ -29,6 +49,12 @@ struct StatsParams { */ Http::Code parse(absl::string_view url, Buffer::Instance& response); + /** + * @return a string representation for a type. + */ + static absl::string_view typeToString(StatsType type); + + StatsType type_{StatsType::All}; bool used_only_{false}; bool prometheus_text_readouts_{false}; bool pretty_{false}; diff --git a/source/server/admin/stats_render.cc b/source/server/admin/stats_render.cc index 9d564db9e72ce..f802d49de5e58 100644 --- a/source/server/admin/stats_render.cc +++ b/source/server/admin/stats_render.cc @@ -1,6 +1,5 @@ #include "source/server/admin/stats_render.h" -#include "source/common/html/utility.h" #include "source/common/json/json_sanitizer.h" #include "source/common/stats/histogram_impl.h" @@ -28,7 +27,7 @@ void StatsTextRender::generate(Buffer::Instance& response, const std::string& na void StatsTextRender::generate(Buffer::Instance& response, const std::string& name, const std::string& value) { - response.addFragments({name, ": \"", Html::Utility::sanitize(value), "\"\n"}); + response.addFragments({name, ": \"", value, "\"\n"}); } void StatsTextRender::generate(Buffer::Instance& response, const std::string& name, diff --git a/source/server/admin/stats_render.h b/source/server/admin/stats_render.h index 9126d619df78e..477c3eb6695e4 100644 --- a/source/server/admin/stats_render.h +++ b/source/server/admin/stats_render.h @@ -32,6 +32,9 @@ class StatsRender { // Completes rendering any buffered data. virtual void finalize(Buffer::Instance& response) PURE; + + // Indicates that no stats for a particular type have been found. + virtual void noStats(Buffer::Instance&, absl::string_view) {} }; // Implements the Render interface for simple textual representation of stats. diff --git a/source/server/admin/stats_request.cc b/source/server/admin/stats_request.cc index 2d5f5dc21c619..c2197261e2835 100644 --- a/source/server/admin/stats_request.cc +++ b/source/server/admin/stats_request.cc @@ -1,10 +1,29 @@ #include "source/server/admin/stats_request.h" +#ifdef ENVOY_ADMIN_HTML +#include "source/server/admin/stats_html_render.h" +#endif + namespace Envoy { namespace Server { -StatsRequest::StatsRequest(Stats::Store& stats, const StatsParams& params) - : params_(params), stats_(stats) {} +StatsRequest::StatsRequest(Stats::Store& stats, const StatsParams& params, + UrlHandlerFn url_handler_fn) + : params_(params), stats_(stats), url_handler_fn_(url_handler_fn) { + switch (params_.type_) { + case StatsType::TextReadouts: + case StatsType::All: + phase_ = Phase::TextReadouts; + break; + case StatsType::Counters: + case StatsType::Gauges: + phase_ = Phase::CountersAndGauges; + break; + case StatsType::Histograms: + phase_ = Phase::Histograms; + break; + } +} Http::Code StatsRequest::start(Http::ResponseHeaderMap& response_headers) { switch (params_.format_) { @@ -14,6 +33,18 @@ Http::Code StatsRequest::start(Http::ResponseHeaderMap& response_headers) { case StatsFormat::Text: render_ = std::make_unique(params_); break; +#ifdef ENVOY_ADMIN_HTML + case StatsFormat::Html: { + auto html_render = std::make_unique(response_headers, response_, params_); + html_render->setSubmitOnChange(true); + html_render->tableBegin(response_); + html_render->urlHandler(response_, url_handler_fn_(), params_.query_); + html_render->tableEnd(response_); + html_render->startPre(response_); + render_.reset(html_render.release()); + break; + } +#endif case StatsFormat::Prometheus: // TODO(#16139): once Prometheus shares this algorithm here, this becomes a legitimate choice. IS_ENVOY_BUG("reached Prometheus case in switch unexpectedly"); @@ -45,13 +76,24 @@ bool StatsRequest::nextChunk(Buffer::Instance& response) { const uint64_t starting_response_length = response.length(); while (response.length() - starting_response_length < chunk_size_) { while (stat_map_.empty()) { + if (phase_stat_count_ == 0) { + render_->noStats(response, phase_string_); + } else { + phase_stat_count_ = 0; + } + if (params_.type_ != StatsType::All) { + render_->finalize(response); + return false; + } switch (phase_) { case Phase::TextReadouts: phase_ = Phase::CountersAndGauges; + phase_string_ = "Counters and Gauges"; startPhase(); break; case Phase::CountersAndGauges: phase_ = Phase::Histograms; + phase_string_ = "Histograms"; startPhase(); break; case Phase::Histograms: @@ -75,20 +117,24 @@ bool StatsRequest::nextChunk(Buffer::Instance& response) { case StatOrScopesIndex::TextReadout: renderStat(iter->first, response, variant); stat_map_.erase(iter); + ++phase_stat_count_; break; case StatOrScopesIndex::Counter: renderStat(iter->first, response, variant); stat_map_.erase(iter); + ++phase_stat_count_; break; case StatOrScopesIndex::Gauge: renderStat(iter->first, response, variant); stat_map_.erase(iter); + ++phase_stat_count_; break; case StatOrScopesIndex::Histogram: { auto histogram = absl::get(variant); auto parent_histogram = dynamic_cast(histogram.get()); if (parent_histogram != nullptr) { render_->generate(response, iter->first, *parent_histogram); + ++phase_stat_count_; } stat_map_.erase(iter); } @@ -118,8 +164,12 @@ void StatsRequest::populateStatsForCurrentPhase(const ScopeVec& scope_vec) { populateStatsFromScopes(scope_vec); break; case Phase::CountersAndGauges: - populateStatsFromScopes(scope_vec); - populateStatsFromScopes(scope_vec); + if (params_.type_ != StatsType::Gauges) { + populateStatsFromScopes(scope_vec); + } + if (params_.type_ != StatsType::Counters) { + populateStatsFromScopes(scope_vec); + } break; case Phase::Histograms: populateStatsFromScopes(scope_vec); diff --git a/source/server/admin/stats_request.h b/source/server/admin/stats_request.h index 6d4226abe4cb4..41d43632a83fe 100644 --- a/source/server/admin/stats_request.h +++ b/source/server/admin/stats_request.h @@ -35,9 +35,12 @@ class StatsRequest : public Admin::Request { }; public: + using UrlHandlerFn = std::function; + static constexpr uint64_t DefaultChunkSize = 2 * 1000 * 1000; - StatsRequest(Stats::Store& stats, const StatsParams& params); + StatsRequest(Stats::Store& stats, const StatsParams& params, + UrlHandlerFn url_handler_fn = nullptr); // Admin::Request Http::Code start(Http::ResponseHeaderMap& response_headers) override; @@ -104,7 +107,10 @@ class StatsRequest : public Admin::Request { ScopeVec scopes_; absl::btree_map stat_map_; Phase phase_{Phase::TextReadouts}; + uint64_t phase_stat_count_{0}; + absl::string_view phase_string_{"text readouts"}; Buffer::OwnedImpl response_; + UrlHandlerFn url_handler_fn_; uint64_t chunk_size_{DefaultChunkSize}; }; diff --git a/source/server/admin/utils.cc b/source/server/admin/utils.cc index b734595c770f5..3dd500a410668 100644 --- a/source/server/admin/utils.cc +++ b/source/server/admin/utils.cc @@ -63,16 +63,15 @@ absl::Status histogramBucketsParam(const Http::Utility::QueryParams& params, HistogramBucketsMode& histogram_buckets_mode) { absl::optional histogram_buckets_query_param = queryParam(params, "histogram_buckets"); + histogram_buckets_mode = HistogramBucketsMode::NoBuckets; if (histogram_buckets_query_param.has_value()) { if (histogram_buckets_query_param.value() == "cumulative") { 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() == "none") { - histogram_buckets_mode = HistogramBucketsMode::NoBuckets; - } else { + } else if (histogram_buckets_query_param.value() != "none") { return absl::InvalidArgumentError( - "usage: /stats?histogram_buckets=cumulative or /stats?histogram_buckets=disjoint \n"); + "usage: /stats?histogram_buckets=(cumulative|disjoint|none)\n"); } } return absl::OkStatus(); diff --git a/source/server/config_validation/admin.cc b/source/server/config_validation/admin.cc index 514a5ed96dc79..678942a2934b0 100644 --- a/source/server/config_validation/admin.cc +++ b/source/server/config_validation/admin.cc @@ -5,10 +5,12 @@ namespace Server { // Pretend that handler was added successfully. bool ValidationAdmin::addStreamingHandler(const std::string&, const std::string&, GenRequestFn, - bool, bool) { + bool, bool, const ParamDescriptorVec&) { return true; } -bool ValidationAdmin::addHandler(const std::string&, const std::string&, HandlerCb, bool, bool) { + +bool ValidationAdmin::addHandler(const std::string&, const std::string&, HandlerCb, bool, bool, + const ParamDescriptorVec&) { return true; } diff --git a/source/server/config_validation/admin.h b/source/server/config_validation/admin.h index 7abe5e5561546..ca9a0a44a8ae5 100644 --- a/source/server/config_validation/admin.h +++ b/source/server/config_validation/admin.h @@ -23,9 +23,10 @@ class ValidationAdmin : public Admin { : socket_(address ? std::make_shared(nullptr, std::move(address), nullptr) : nullptr) {} - bool addHandler(const std::string&, const std::string&, HandlerCb, bool, bool) override; - bool addStreamingHandler(const std::string&, const std::string&, GenRequestFn, bool, - bool) override; + bool addHandler(const std::string&, const std::string&, HandlerCb, bool, bool, + const ParamDescriptorVec& = {}) override; + bool addStreamingHandler(const std::string&, const std::string&, GenRequestFn, bool, bool, + const ParamDescriptorVec& = {}) override; bool removeHandler(const std::string&) override; const Network::Socket& socket() override; ConfigTracker& getConfigTracker() override; diff --git a/test/extensions/common/tap/admin_test.cc b/test/extensions/common/tap/admin_test.cc index 43d23635d4d9f..12610102208e3 100644 --- a/test/extensions/common/tap/admin_test.cc +++ b/test/extensions/common/tap/admin_test.cc @@ -70,7 +70,7 @@ class BaseAdminHandlerTest : public testing::Test { public: void setup(Network::Address::Type socket_type = Network::Address::Type::Ip) { ON_CALL(admin_.socket_, addressType()).WillByDefault(Return(socket_type)); - EXPECT_CALL(admin_, addHandler("/tap", "tap filter control", _, true, true)) + EXPECT_CALL(admin_, addHandler("/tap", "tap filter control", _, true, true, _)) .WillOnce(DoAll(SaveArg<2>(&cb_), Return(true))); EXPECT_CALL(admin_, socket()); handler_ = std::make_unique(admin_, main_thread_dispatcher_); diff --git a/test/extensions/filters/http/common/fuzz/uber_per_filter.cc b/test/extensions/filters/http/common/fuzz/uber_per_filter.cc index 3d3722c9d8150..e72e1cb322d9a 100644 --- a/test/extensions/filters/http/common/fuzz/uber_per_filter.cc +++ b/test/extensions/filters/http/common/fuzz/uber_per_filter.cc @@ -161,7 +161,8 @@ void UberFilterFuzzer::perFilterSetup() { // Prepare expectations for TAP config. ON_CALL(factory_context_, admin()).WillByDefault(testing::ReturnRef(factory_context_.admin_)); - ON_CALL(factory_context_.admin_, addHandler(_, _, _, _, _)).WillByDefault(testing::Return(true)); + ON_CALL(factory_context_.admin_, addHandler(_, _, _, _, _, _)) + .WillByDefault(testing::Return(true)); ON_CALL(factory_context_.admin_, removeHandler(_)).WillByDefault(testing::Return(true)); // Prepare expectations for WASM filter. diff --git a/test/mocks/server/admin.cc b/test/mocks/server/admin.cc index db35d25034e4f..4a41b1887f7d8 100644 --- a/test/mocks/server/admin.cc +++ b/test/mocks/server/admin.cc @@ -14,7 +14,7 @@ MockAdmin::MockAdmin() { ON_CALL(*this, getConfigTracker()).WillByDefault(ReturnRef(config_tracker_)); ON_CALL(*this, concurrency()).WillByDefault(Return(1)); ON_CALL(*this, socket()).WillByDefault(ReturnRef(socket_)); - ON_CALL(*this, addHandler(_, _, _, _, _)).WillByDefault(Return(true)); + ON_CALL(*this, addHandler(_, _, _, _, _, _)).WillByDefault(Return(true)); ON_CALL(*this, removeHandler(_)).WillByDefault(Return(true)); } diff --git a/test/mocks/server/admin.h b/test/mocks/server/admin.h index 60e7fe6db8e37..960eb974d17d0 100644 --- a/test/mocks/server/admin.h +++ b/test/mocks/server/admin.h @@ -23,10 +23,10 @@ class MockAdmin : public Admin { // Server::Admin MOCK_METHOD(bool, addHandler, (const std::string& prefix, const std::string& help_text, HandlerCb callback, - bool removable, bool mutates_server_state)); + bool removable, bool mutates_server_state, const ParamDescriptorVec& params)); MOCK_METHOD(bool, addStreamingHandler, (const std::string& prefix, const std::string& help_text, GenRequestFn callback, - bool removable, bool mutates_server_state)); + bool removable, bool mutates_server_state, const ParamDescriptorVec& params)); MOCK_METHOD(bool, removeHandler, (const std::string& prefix)); MOCK_METHOD(Network::Socket&, socket, ()); MOCK_METHOD(ConfigTracker&, getConfigTracker, ()); diff --git a/test/server/admin/BUILD b/test/server/admin/BUILD index b8789d0bcc8fa..35684d1b00382 100644 --- a/test/server/admin/BUILD +++ b/test/server/admin/BUILD @@ -89,6 +89,7 @@ envoy_cc_test( name = "stats_params_test", srcs = ["stats_params_test.cc"], deps = [ + "//source/common/buffer:buffer_lib", "//source/server/admin:stats_params_lib", "//test/test_common:test_runtime_lib", ], @@ -96,10 +97,22 @@ envoy_cc_test( envoy_cc_test( name = "stats_render_test", - srcs = ["stats_render_test.cc"], + srcs = ["stats_render_test.cc"] + envoy_select_admin_html([ + "stats_html_render_test.cc", + ]), + deps = [ + ":stats_render_test_base", + "//source/server/admin:admin_lib", + ], +) + +envoy_cc_test_library( + name = "stats_render_test_base", + srcs = ["stats_render_test_base.cc"], + hdrs = ["stats_render_test_base.h"], deps = [ "//source/common/stats:thread_local_store_lib", - "//source/server/admin:stats_render_lib", + "//source/server/admin:stats_request_lib", "//test/mocks/event:event_mocks", "//test/mocks/stats:stats_mocks", "//test/mocks/thread_local:thread_local_mocks", diff --git a/test/server/admin/admin_html_test.cc b/test/server/admin/admin_html_test.cc index 904407cc56e17..feae8252e6afd 100644 --- a/test/server/admin/admin_html_test.cc +++ b/test/server/admin/admin_html_test.cc @@ -31,14 +31,12 @@ TEST_P(AdminInstanceTest, EscapeHelpTextWithPunctuation) { EXPECT_NE(-1, response.search(escaped_planets.data(), escaped_planets.size(), 0, 0)); } -TEST_P(AdminInstanceTest, HelpUsesFormForMutations) { +TEST_P(AdminInstanceTest, HelpUsesPostForMutations) { Http::TestResponseHeaderMapImpl header_map; Buffer::OwnedImpl response; EXPECT_EQ(Http::Code::OK, getCallback("/", header_map, response)); - const std::string logging_action = "
counterFromString("c1"); Stats::Counter& c2 = store_->counterFromString("c2"); @@ -182,6 +183,43 @@ TEST_F(AdminStatsTest, HandlerStatsPlainText) { EXPECT_EQ(expected, code_response.second); } +#ifdef ENVOY_ADMIN_HTML +TEST_F(AdminStatsTest, HandlerStatsHtml) { + InSequence s; + store_->initializeThreading(main_thread_dispatcher_, tls_); + + store_->counterFromStatName(makeStat("foo.c0")).add(0); + Stats::ScopeSharedPtr scope0 = store_->createScope(""); + store_->counterFromStatName(makeStat("foo.c1")).add(1); + Stats::ScopeSharedPtr scope = store_->createScope("scope"); + scope->gaugeFromStatName(makeStat("g2"), Stats::Gauge::ImportMode::Accumulate).set(2); + Stats::ScopeSharedPtr scope2 = store_->createScope("scope1.scope2"); + scope2->textReadoutFromStatName(makeStat("t3")).set("text readout value"); + scope2->counterFromStatName(makeStat("unset")); + + auto test = [this](absl::string_view params, const std::vector& expected, + const std::vector& not_expected) { + std::string url = absl::StrCat("/stats?format=html", params); + CodeResponse code_response = handlerStats(url); + EXPECT_EQ(Http::Code::OK, code_response.first); + for (const std::string& expect : expected) { + EXPECT_THAT(code_response.second, HasSubstr(expect)) << "params=" << params; + } + for (const std::string& not_expect : not_expected) { + EXPECT_THAT(code_response.second, Not(HasSubstr(not_expect))) << "params=" << params; + } + }; + test("", + {"foo.c0: 0", "foo.c1: 1", "scope.g2: 2", "scope1.scope2.unset: 0", // expected + "scope1.scope2.t3: \"text readout value\"", "No Histograms found"}, + {"No TextReadouts found"}); // not expected + test("&type=Counters", {"foo.c0: 0", "foo.c1: 1"}, // expected + {"No Histograms found", "scope.g2: 2"}); // not expected + test("&usedonly", {"foo.c0: 0", "foo.c1: 1"}, // expected + {"scope1.scope2.unset"}); // not expected +} +#endif + TEST_F(AdminStatsTest, HandlerStatsPlainTextHistogramBucketsCumulative) { const std::string url = "/stats?histogram_buckets=cumulative"; @@ -258,8 +296,7 @@ TEST_F(AdminStatsTest, HandlerStatsPlainTextHistogramBucketsInvalid) { const std::string url = "/stats?histogram_buckets=invalid_input"; CodeResponse code_response = handlerStats(url); EXPECT_EQ(Http::Code::BadRequest, code_response.first); - EXPECT_EQ("usage: /stats?histogram_buckets=cumulative or /stats?histogram_buckets=disjoint \n", - code_response.second); + EXPECT_EQ("usage: /stats?histogram_buckets=(cumulative|disjoint|none)\n", code_response.second); } TEST_F(AdminStatsTest, HandlerStatsJsonNoHistograms) { diff --git a/test/server/admin/stats_html_render_test.cc b/test/server/admin/stats_html_render_test.cc new file mode 100644 index 0000000000000..50e0b85c5f563 --- /dev/null +++ b/test/server/admin/stats_html_render_test.cc @@ -0,0 +1,97 @@ +#include "source/server/admin/admin.h" +#include "source/server/admin/stats_html_render.h" + +#include "test/server/admin/stats_render_test_base.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::HasSubstr; +using testing::Not; + +namespace Envoy { +namespace Server { + +class StatsHtmlRenderTest : public StatsRenderTestBase { +protected: + static Admin::RequestPtr handlerCallback(absl::string_view, AdminStream&) { + return Admin::makeStaticTextRequest("", Http::Code::OK); + } + + const Admin::UrlHandler handler_{ + "/prefix", "help text", handlerCallback, + false, false, {{Admin::ParamDescriptor::Type::Boolean, "param", "param help"}}}; + StatsHtmlRender renderer_{response_headers_, response_, params_}; + Http::Utility::QueryParams query_params_; +}; + +TEST_F(StatsHtmlRenderTest, String) { + EXPECT_THAT(render(renderer_, "name", "abc 123 ~!@#$%^&*()-_=+;:'\",<.>/?"), + HasSubstr("name: \"abc 123 ~!@#$%^&*()-_=+;:'",<.>/?\"\n")); +} + +TEST_F(StatsHtmlRenderTest, HistogramNoBuckets) { + constexpr absl::string_view expected = + "h1: 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_THAT(render<>(renderer_, "h1", populateHistogram("h1", {200, 300, 300})), + HasSubstr(expected)); +} + +TEST_F(StatsHtmlRenderTest, RenderHead) { + // The "..." is rendered by the constructor. + EXPECT_THAT(response_.toString(), HasSubstr("")); + EXPECT_THAT(response_.toString(), HasSubstr("")); +} + +TEST_F(StatsHtmlRenderTest, RenderTableBegin) { + renderer_.tableBegin(response_); + EXPECT_THAT(response_.toString(), HasSubstr("")); +} + +TEST_F(StatsHtmlRenderTest, RenderTableEnd) { + renderer_.tableEnd(response_); + EXPECT_THAT(response_.toString(), HasSubstr("
")); +} + +TEST_F(StatsHtmlRenderTest, RenderUrlHandlerNoQuery) { + renderer_.urlHandler(response_, handler_, query_params_); + std::string out = response_.toString(); + EXPECT_THAT( + out, + HasSubstr(""))); + EXPECT_THAT(out, HasSubstr("help text")); + EXPECT_THAT(out, HasSubstr("param help")); + EXPECT_THAT(out, HasSubstr("")); + EXPECT_THAT(out, Not(HasSubstr(" onchange='prefix.submit()"))); + EXPECT_THAT(out, Not(HasSubstr(" type='hidden' "))); +} + +TEST_F(StatsHtmlRenderTest, RenderUrlHandlerWithQuery) { + query_params_["param"] = "on"; + renderer_.urlHandler(response_, handler_, query_params_); + std::string out = response_.toString(); + EXPECT_THAT( + out, + HasSubstr("")); + EXPECT_THAT(out, HasSubstr("help text")); + EXPECT_THAT(out, HasSubstr("param help")); + EXPECT_THAT(out, HasSubstr("")); + EXPECT_THAT(out, Not(HasSubstr(" onchange='prefix.submit()"))); + EXPECT_THAT(out, Not(HasSubstr(" type='hidden' "))); +} + +TEST_F(StatsHtmlRenderTest, RenderUrlHandlerSubmitOnChange) { + renderer_.setSubmitOnChange(true); + renderer_.urlHandler(response_, handler_, query_params_); + std::string out = response_.toString(); + EXPECT_THAT(out, HasSubstr(" onchange='prefix.submit()")); + EXPECT_THAT(out, Not(HasSubstr(""))); + EXPECT_THAT(out, Not(HasSubstr(" type='hidden' "))); +} + +} // namespace Server +} // namespace Envoy diff --git a/test/server/admin/stats_params_test.cc b/test/server/admin/stats_params_test.cc index 18a2aa031fb62..6dbf9b68d4ac4 100644 --- a/test/server/admin/stats_params_test.cc +++ b/test/server/admin/stats_params_test.cc @@ -8,12 +8,43 @@ namespace Envoy { namespace Server { +TEST(StatsParamsTest, TypeToString) { + EXPECT_EQ("TextReadouts", StatsParams::typeToString(StatsType::TextReadouts)); + EXPECT_EQ("Gauges", StatsParams::typeToString(StatsType::Gauges)); + EXPECT_EQ("Counters", StatsParams::typeToString(StatsType::Counters)); + EXPECT_EQ("Histograms", StatsParams::typeToString(StatsType::Histograms)); + EXPECT_EQ("All", StatsParams::typeToString(StatsType::All)); +} + +TEST(StatsParamsTest, ParseParamsType) { + Buffer::OwnedImpl response; + StatsParams params; + + ASSERT_EQ(Http::Code::OK, params.parse("?type=TextReadouts", response)); + EXPECT_EQ(StatsType::TextReadouts, params.type_); + ASSERT_EQ(Http::Code::OK, params.parse("?type=Gauges", response)); + EXPECT_EQ(StatsType::Gauges, params.type_); + ASSERT_EQ(Http::Code::OK, params.parse("?type=Counters", response)); + EXPECT_EQ(StatsType::Counters, params.type_); + ASSERT_EQ(Http::Code::OK, params.parse("?type=Histograms", response)); + EXPECT_EQ(StatsType::Histograms, params.type_); + ASSERT_EQ(Http::Code::OK, params.parse("?type=All", response)); + EXPECT_EQ(StatsType::All, params.type_); + EXPECT_EQ(Http::Code::BadRequest, params.parse("?type=bogus", response)); +} + TEST(StatsParamsTest, ParseParamsFormat) { Buffer::OwnedImpl response; StatsParams params; ASSERT_EQ(Http::Code::OK, params.parse("?format=text", response)); EXPECT_EQ(StatsFormat::Text, params.format_); +#ifdef ENVOY_ADMIN_HTML + ASSERT_EQ(Http::Code::OK, params.parse("?format=html", response)); + EXPECT_EQ(StatsFormat::Html, params.format_); +#else + EXPECT_EQ(Http::Code::BadRequest, params.parse("?format=html", response)); +#endif ASSERT_EQ(Http::Code::OK, params.parse("?format=json", response)); EXPECT_EQ(StatsFormat::Json, params.format_); ASSERT_EQ(Http::Code::OK, params.parse("?format=prometheus", response)); diff --git a/test/server/admin/stats_render_test.cc b/test/server/admin/stats_render_test.cc index 7770b42d69537..b2f9ce2231531 100644 --- a/test/server/admin/stats_render_test.cc +++ b/test/server/admin/stats_render_test.cc @@ -1,61 +1,11 @@ #include -#include "source/common/buffer/buffer_impl.h" -#include "source/common/stats/thread_local_store.h" -#include "source/server/admin/stats_render.h" - -#include "test/mocks/event/mocks.h" -#include "test/mocks/stats/mocks.h" -#include "test/mocks/thread_local/mocks.h" -#include "test/test_common/utility.h" - -#include "gtest/gtest.h" - -using testing::NiceMock; +#include "test/server/admin/stats_render_test_base.h" namespace Envoy { namespace Server { -class StatsRenderTest : public testing::Test { -protected: - StatsRenderTest() : alloc_(symbol_table_), store_(alloc_) { - store_.addSink(sink_); - store_.initializeThreading(main_thread_dispatcher_, tls_); - } - - ~StatsRenderTest() override { - tls_.shutdownGlobalThreading(); - store_.shutdownThreading(); - tls_.shutdownThread(); - } - - template - std::string render(StatsRender& render, absl::string_view name, const T& value) { - render.generate(response_, std::string(name), value); - render.finalize(response_); - return response_.toString(); - } - - Stats::ParentHistogram& populateHistogram(const std::string& name, - const std::vector& vals) { - Stats::Histogram& h = store_.histogramFromString(name, Stats::Histogram::Unit::Unspecified); - for (uint64_t val : vals) { - h.recordValue(val); - } - store_.mergeHistograms([]() -> void {}); - return dynamic_cast(h); - } - - Stats::SymbolTableImpl symbol_table_; - Stats::AllocatorImpl alloc_; - NiceMock sink_; - NiceMock main_thread_dispatcher_; - NiceMock tls_; - Stats::ThreadLocalStoreImpl store_; - Http::TestResponseHeaderMapImpl response_headers_; - Buffer::OwnedImpl response_; - StatsParams params_; -}; +using StatsRenderTest = StatsRenderTestBase; TEST_F(StatsRenderTest, TextInt) { StatsTextRender renderer(params_); @@ -64,7 +14,7 @@ TEST_F(StatsRenderTest, TextInt) { TEST_F(StatsRenderTest, TextString) { StatsTextRender renderer(params_); - EXPECT_EQ("name: \"abc 123 ~!@#$%^&*()-_=+;:'",<.>/?\"\n", + EXPECT_EQ("name: \"abc 123 ~!@#$%^&*()-_=+;:'\",<.>/?\"\n", render(renderer, "name", "abc 123 ~!@#$%^&*()-_=+;:'\",<.>/?")); } diff --git a/test/server/admin/stats_render_test_base.cc b/test/server/admin/stats_render_test_base.cc new file mode 100644 index 0000000000000..23923da1e00c6 --- /dev/null +++ b/test/server/admin/stats_render_test_base.cc @@ -0,0 +1,28 @@ +#include "test/server/admin/stats_render_test_base.h" + +namespace Envoy { +namespace Server { + +StatsRenderTestBase::StatsRenderTestBase() : alloc_(symbol_table_), store_(alloc_) { + store_.addSink(sink_); + store_.initializeThreading(main_thread_dispatcher_, tls_); +} + +StatsRenderTestBase::~StatsRenderTestBase() { + tls_.shutdownGlobalThreading(); + store_.shutdownThreading(); + tls_.shutdownThread(); +} + +Stats::ParentHistogram& StatsRenderTestBase::populateHistogram(const std::string& name, + const std::vector& vals) { + Stats::Histogram& h = store_.histogramFromString(name, Stats::Histogram::Unit::Unspecified); + for (uint64_t val : vals) { + h.recordValue(val); + } + store_.mergeHistograms([]() -> void {}); + return dynamic_cast(h); +} + +} // namespace Server +} // namespace Envoy diff --git a/test/server/admin/stats_render_test_base.h b/test/server/admin/stats_render_test_base.h new file mode 100644 index 0000000000000..0c566f677be00 --- /dev/null +++ b/test/server/admin/stats_render_test_base.h @@ -0,0 +1,44 @@ +#pragma once + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/stats/thread_local_store.h" +#include "source/server/admin/stats_render.h" + +#include "test/mocks/event/mocks.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Server { + +class StatsRenderTestBase : public testing::Test { +protected: + StatsRenderTestBase(); + ~StatsRenderTestBase() override; + + template + std::string render(StatsRender& render, absl::string_view name, const T& value) { + render.generate(response_, std::string(name), value); + render.finalize(response_); + return response_.toString(); + } + + Stats::ParentHistogram& populateHistogram(const std::string& name, + const std::vector& vals); + + Stats::SymbolTableImpl symbol_table_; + Stats::AllocatorImpl alloc_; + testing::NiceMock sink_; + testing::NiceMock main_thread_dispatcher_; + testing::NiceMock tls_; + Stats::ThreadLocalStoreImpl store_; + Http::TestResponseHeaderMapImpl response_headers_; + Buffer::OwnedImpl response_; + StatsParams params_; +}; + +} // namespace Server +} // namespace Envoy diff --git a/test/server/admin/stats_request_test.cc b/test/server/admin/stats_request_test.cc index eae73e72ee337..a130879f9a274 100644 --- a/test/server/admin/stats_request_test.cc +++ b/test/server/admin/stats_request_test.cc @@ -30,20 +30,23 @@ class StatsRequestTest : public testing::Test { tls_.shutdownThread(); } - std::unique_ptr makeRequest(bool used_only, bool json) { + std::unique_ptr makeRequest(bool used_only, StatsFormat format, StatsType type) { StatsParams params; params.used_only_ = used_only; - if (json) { - params.format_ = StatsFormat::Json; - } + params.type_ = type; + params.format_ = format; return std::make_unique(store_, params); } // Executes a request, counting the chunks that were generated. - uint32_t iterateChunks(StatsRequest& request, bool drain = true) { + uint32_t iterateChunks(StatsRequest& request, bool drain = true, + Http::Code expect_code = Http::Code::OK) { Http::TestResponseHeaderMapImpl response_headers; Http::Code code = request.start(response_headers); - EXPECT_EQ(Http::Code::OK, code); + EXPECT_EQ(expect_code, code); + if (code != Http::Code::OK) { + return 0; + } Buffer::OwnedImpl data; uint32_t num_chunks = 0; bool more = true; @@ -83,38 +86,48 @@ class StatsRequestTest : public testing::Test { Buffer::OwnedImpl response_; }; -TEST_F(StatsRequestTest, Empty) { EXPECT_EQ(0, iterateChunks(*makeRequest(false, false))); } +TEST_F(StatsRequestTest, Empty) { + EXPECT_EQ(0, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::All))); +} TEST_F(StatsRequestTest, OneCounter) { store_.counterFromStatName(makeStatName("foo")); - EXPECT_EQ(1, iterateChunks(*makeRequest(false, false))); + EXPECT_EQ(1, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::All))); + EXPECT_EQ(1, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::Counters))); + EXPECT_EQ(0, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::Gauges))); } TEST_F(StatsRequestTest, OneGauge) { store_.gaugeFromStatName(makeStatName("foo"), Stats::Gauge::ImportMode::Accumulate); - EXPECT_EQ(1, iterateChunks(*makeRequest(false, false))); + EXPECT_EQ(1, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::All))); + EXPECT_EQ(1, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::Gauges))); + EXPECT_EQ(0, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::Counters))); } TEST_F(StatsRequestTest, OneHistogram) { store_.histogramFromStatName(makeStatName("foo"), Stats::Histogram::Unit::Milliseconds); - EXPECT_EQ(1, iterateChunks(*makeRequest(false, false))); + EXPECT_EQ(1, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::All))); + EXPECT_EQ(1, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::Histograms))); + EXPECT_EQ(0, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::Counters))); } TEST_F(StatsRequestTest, OneTextReadout) { store_.textReadoutFromStatName(makeStatName("foo")); - EXPECT_EQ(1, iterateChunks(*makeRequest(false, false))); + EXPECT_EQ(1, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::All))); + EXPECT_EQ(1, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::TextReadouts))); + EXPECT_EQ(0, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::Counters))); } TEST_F(StatsRequestTest, OneScope) { Stats::ScopeSharedPtr scope = store_.createScope("foo"); - EXPECT_EQ(0, iterateChunks(*makeRequest(false, false))); + EXPECT_EQ(0, iterateChunks(*makeRequest(false, StatsFormat::Text, StatsType::All))); } TEST_F(StatsRequestTest, ManyStatsSmallChunkSize) { for (uint32_t i = 0; i < 100; ++i) { store_.counterFromStatName(makeStatName(absl::StrCat("foo", i))); } - std::unique_ptr request = makeRequest(false, false); + std::unique_ptr request = makeRequest(false, StatsFormat::Text, StatsType::All); request->setChunkSize(100); EXPECT_EQ(9, iterateChunks(*request)); } @@ -123,19 +136,30 @@ TEST_F(StatsRequestTest, ManyStatsSmallChunkSizeNoDrain) { for (uint32_t i = 0; i < 100; ++i) { store_.counterFromStatName(makeStatName(absl::StrCat("foo", i))); } - std::unique_ptr request = makeRequest(false, false); + std::unique_ptr request = makeRequest(false, StatsFormat::Text, StatsType::All); request->setChunkSize(100); EXPECT_EQ(9, iterateChunks(*request, false)); } TEST_F(StatsRequestTest, OneStatUsedOnly) { store_.counterFromStatName(makeStatName("foo")); - EXPECT_EQ(0, iterateChunks(*makeRequest(true, false))); + EXPECT_EQ(0, iterateChunks(*makeRequest(true, StatsFormat::Text, StatsType::All))); } TEST_F(StatsRequestTest, OneStatJson) { store_.counterFromStatName(makeStatName("foo")); - EXPECT_THAT(response(*makeRequest(false, true)), StartsWith("{")); + EXPECT_THAT(response(*makeRequest(false, StatsFormat::Json, StatsType::All)), StartsWith("{")); +} + +TEST_F(StatsRequestTest, OneStatPrometheus) { + // Currently the rendering infrastructure does not support Prometheus -- that + // gets rendered using a different code-path. This will be fixed at some + // point, to make Prometheus consume less resource, and when that occurs this + // test can exercise that. + store_.counterFromStatName(makeStatName("foo")); + EXPECT_ENVOY_BUG(iterateChunks(*makeRequest(false, StatsFormat::Prometheus, StatsType::All), true, + Http::Code::BadRequest), + "reached Prometheus case in switch unexpectedly"); } } // namespace Server diff --git a/test/server/admin/utils_test.cc b/test/server/admin/utils_test.cc index 047a188df0e4d..8a97da84a96bf 100644 --- a/test/server/admin/utils_test.cc +++ b/test/server/admin/utils_test.cc @@ -1,22 +1,50 @@ +#include "envoy/http/query_params.h" + #include "source/server/admin/utils.h" #include "test/test_common/utility.h" +#include "gtest/gtest.h" + +using testing::HasSubstr; + namespace Envoy { namespace Server { class UtilsTest : public testing::Test { -public: +protected: UtilsTest() = default; + + Http::Utility::QueryParams query_; }; // Most utils paths are covered through other tests, these tests take of // of special cases to get remaining coverage. -TEST(UtilsTest, BadServerState) { +TEST_F(UtilsTest, BadServerState) { Utility::serverState(Init::Manager::State::Uninitialized, true); EXPECT_ENVOY_BUG(Utility::serverState(static_cast(123), true), "unexpected server state"); } +TEST_F(UtilsTest, HistogramMode) { + Utility::HistogramBucketsMode histogram_buckets_mode = Utility::HistogramBucketsMode::Cumulative; + EXPECT_TRUE(Utility::histogramBucketsParam(query_, histogram_buckets_mode).ok()); + EXPECT_EQ(Utility::HistogramBucketsMode::NoBuckets, histogram_buckets_mode); + query_["histogram_buckets"] = "none"; + EXPECT_TRUE(Utility::histogramBucketsParam(query_, histogram_buckets_mode).ok()); + EXPECT_EQ(Utility::HistogramBucketsMode::NoBuckets, histogram_buckets_mode); + query_["histogram_buckets"] = "cumulative"; + EXPECT_TRUE(Utility::histogramBucketsParam(query_, histogram_buckets_mode).ok()); + EXPECT_EQ(Utility::HistogramBucketsMode::Cumulative, histogram_buckets_mode); + query_["histogram_buckets"] = "disjoint"; + EXPECT_TRUE(Utility::histogramBucketsParam(query_, histogram_buckets_mode).ok()); + EXPECT_EQ(Utility::HistogramBucketsMode::Disjoint, histogram_buckets_mode); + query_["histogram_buckets"] = "garbage"; + absl::Status status = Utility::histogramBucketsParam(query_, histogram_buckets_mode); + EXPECT_FALSE(status.ok()); + EXPECT_THAT(status.ToString(), + HasSubstr("usage: /stats?histogram_buckets=(cumulative|disjoint|none)")); +} + } // namespace Server } // namespace Envoy