From b02c6b1c804cb3dccc93c4450ec109ead09fe86e Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 2 Nov 2020 06:30:40 -0600 Subject: [PATCH] Issue #5539 - Proper StatisticsServlet output format via content negotiation Signed-off-by: Joakim Erdfelt --- jetty-servlet/pom.xml | 5 + .../jetty/servlet/StatisticsServlet.java | 610 +++++++++++++----- .../jetty/servlet/StatisticsServletTest.java | 202 +++++- 3 files changed, 646 insertions(+), 171 deletions(-) diff --git a/jetty-servlet/pom.xml b/jetty-servlet/pom.xml index 4d6dd0df7df3..0c155db9d074 100644 --- a/jetty-servlet/pom.xml +++ b/jetty-servlet/pom.xml @@ -41,6 +41,11 @@ jetty-security ${project.version} + + org.eclipse.jetty + jetty-util-ajax + ${project.version} + org.eclipse.jetty jetty-jmx diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java index 55fd67963aeb..3c6f325dd8b4 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java @@ -19,29 +19,41 @@ package org.eclipse.jetty.servlet; import java.io.IOException; -import java.io.PrintWriter; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.QuotedQualityCSV; import org.eclipse.jetty.io.ConnectionStatistics; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.ConnectorStatistics; -import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.StatisticsHandler; -import org.eclipse.jetty.util.component.Container; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.ajax.JSON; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import static java.nio.charset.StandardCharsets.UTF_8; + public class StatisticsServlet extends HttpServlet { private static final Logger LOG = Log.getLogger(StatisticsServlet.class); @@ -49,7 +61,7 @@ public class StatisticsServlet extends HttpServlet boolean _restrictToLocalhost = true; // defaults to true private StatisticsHandler _statsHandler; private MemoryMXBean _memoryBean; - private Connector[] _connectors; + private List _connectors; @Override public void init() throws ServletException @@ -58,20 +70,16 @@ public void init() throws ServletException ContextHandler.Context scontext = (ContextHandler.Context)context; Server server = scontext.getContextHandler().getServer(); - Handler handler = server.getChildHandlerByClass(StatisticsHandler.class); + _statsHandler = server.getChildHandlerByClass(StatisticsHandler.class); - if (handler != null) - { - _statsHandler = (StatisticsHandler)handler; - } - else + if (_statsHandler == null) { LOG.warn("Statistics Handler not installed!"); return; } _memoryBean = ManagementFactory.getMemoryMXBean(); - _connectors = server.getConnectors(); + _connectors = Arrays.asList(server.getConnectors()); if (getInitParameter("restrictToLocalhost") != null) { @@ -80,47 +88,150 @@ public void init() throws ServletException } @Override - public void doPost(HttpServletRequest sreq, HttpServletResponse sres) throws ServletException, IOException + public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - doGet(sreq, sres); + doGet(request, response); } @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (_statsHandler == null) { LOG.warn("Statistics Handler not installed!"); - resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); return; } if (_restrictToLocalhost) { - if (!isLoopbackAddress(req.getRemoteAddr())) + if (!isLoopbackAddress(request.getRemoteAddr())) { - resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } } - if (Boolean.parseBoolean(req.getParameter("statsReset"))) + if (Boolean.parseBoolean(request.getParameter("statsReset"))) { + response.setStatus(HttpServletResponse.SC_OK); _statsHandler.statsReset(); return; } - String wantXml = req.getParameter("xml"); - if (wantXml == null) - wantXml = req.getParameter("XML"); + List acceptable = getOrderedAcceptableMimeTypes(request); + + for (String mimeType : acceptable) + { + switch (mimeType) + { + case "application/json": + writeJsonResponse(response); + return; + case "text/xml": + writeXmlResponse(response); + return; + case "text/html": + writeHtmlResponse(response); + return; + case "text/plain": + writeTextResponse(response); + return; + case "*/*": + String wantXml = request.getParameter("xml"); + if (wantXml == null) + wantXml = request.getParameter("XML"); + + if (Boolean.parseBoolean(wantXml)) + { + writeXmlResponse(response); + } + else + { + writeTextResponse(response); + } + return; + default: + if (LOG.isDebugEnabled()) + { + LOG.debug("Ignoring unrecognized mime-type {}", mimeType); + } + break; + } + } + // None of the listed `Accept` mime-types were found. + response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE); + } + + private void writeTextResponse(HttpServletResponse response) throws IOException + { + response.setCharacterEncoding("utf-8"); + response.setContentType("text/plain"); + CharSequence text = generateResponse(new TextProducer()); + response.getWriter().print(text.toString()); + } + + private void writeHtmlResponse(HttpServletResponse response) throws IOException + { + response.setCharacterEncoding("utf-8"); + response.setContentType("text/html"); + Writer htmlWriter = new OutputStreamWriter(response.getOutputStream(), UTF_8); + htmlWriter.append(""); + htmlWriter.append(this.getClass().getSimpleName()); + htmlWriter.append("\n"); + CharSequence html = generateResponse(new HtmlProducer()); + htmlWriter.append(html.toString()); + htmlWriter.append("\n\n"); + htmlWriter.flush(); + } + + private void writeXmlResponse(HttpServletResponse response) throws IOException + { + response.setCharacterEncoding("utf-8"); + response.setContentType("text/xml"); + CharSequence xml = generateResponse(new XmlProducer()); + response.getWriter().print(xml.toString()); + } - if (Boolean.parseBoolean(wantXml)) + private void writeJsonResponse(HttpServletResponse response) throws IOException + { + // We intentionally don't put "UTF-8" into the response headers + // as the rules for application/json state that it should never be + // present on the HTTP Content-Type header. + // It is also true that the application/json mime-type is always UTF-8. + response.setContentType("application/json"); + CharSequence json = generateResponse(new JsonProducer()); + Writer jsonWriter = new OutputStreamWriter(response.getOutputStream(), UTF_8); + jsonWriter.append(json); + jsonWriter.flush(); + } + + private List getOrderedAcceptableMimeTypes(HttpServletRequest request) + { + QuotedQualityCSV values = null; + Enumeration enumAccept = request.getHeaders("Accept"); + if (enumAccept != null) { - sendXmlResponse(resp); + while (enumAccept.hasMoreElements()) + { + String value = enumAccept.nextElement(); + if (StringUtil.isNotBlank(value)) + { + if (values == null) + { + values = new QuotedQualityCSV(QuotedQualityCSV.MOST_SPECIFIC_MIME_ORDERING); + } + values.addValue(value); + } + } } - else + + if (values != null) { - sendTextResponse(resp); + return values.getValues(); } + + // No accept specified, return that we allow ALL mime types + return Collections.singletonList("*/*"); } private boolean isLoopbackAddress(String address) @@ -137,177 +248,362 @@ private boolean isLoopbackAddress(String address) } } - private void sendXmlResponse(HttpServletResponse response) throws IOException + private CharSequence generateResponse(OutputProducer outputProducer) { - StringBuilder sb = new StringBuilder(); - - sb.append("\n"); - - sb.append(" \n"); - sb.append(" ").append(_statsHandler.getStatsOnMs()).append("\n"); - - sb.append(" ").append(_statsHandler.getRequests()).append("\n"); - sb.append(" ").append(_statsHandler.getRequestsActive()).append("\n"); - sb.append(" ").append(_statsHandler.getRequestsActiveMax()).append("\n"); - sb.append(" ").append(_statsHandler.getRequestTimeTotal()).append("\n"); - sb.append(" ").append(_statsHandler.getRequestTimeMean()).append("\n"); - sb.append(" ").append(_statsHandler.getRequestTimeMax()).append("\n"); - sb.append(" ").append(_statsHandler.getRequestTimeStdDev()).append("\n"); - - sb.append(" ").append(_statsHandler.getDispatched()).append("\n"); - sb.append(" ").append(_statsHandler.getDispatchedActive()).append("\n"); - sb.append(" ").append(_statsHandler.getDispatchedActiveMax()).append("\n"); - sb.append(" ").append(_statsHandler.getDispatchedTimeTotal()).append("\n"); - sb.append(" ").append(_statsHandler.getDispatchedTimeMean()).append("\n"); - sb.append(" ").append(_statsHandler.getDispatchedTimeMax()).append("\n"); - sb.append(" ").append(_statsHandler.getDispatchedTimeStdDev()).append("\n"); - - sb.append(" ").append(_statsHandler.getAsyncRequests()).append("\n"); - sb.append(" ").append(_statsHandler.getAsyncRequestsWaiting()).append("\n"); - sb.append(" ").append(_statsHandler.getAsyncRequestsWaitingMax()).append("\n"); - sb.append(" ").append(_statsHandler.getAsyncDispatches()).append("\n"); - sb.append(" ").append(_statsHandler.getExpires()).append("\n"); - sb.append(" \n"); - - sb.append(" \n"); - sb.append(" ").append(_statsHandler.getResponses1xx()).append("\n"); - sb.append(" ").append(_statsHandler.getResponses2xx()).append("\n"); - sb.append(" ").append(_statsHandler.getResponses3xx()).append("\n"); - sb.append(" ").append(_statsHandler.getResponses4xx()).append("\n"); - sb.append(" ").append(_statsHandler.getResponses5xx()).append("\n"); - sb.append(" ").append(_statsHandler.getResponsesBytesTotal()).append("\n"); - sb.append(" \n"); - - sb.append(" \n"); - for (Connector connector : _connectors) - { - sb.append(" \n"); - sb.append(" ").append(connector.getClass().getName()).append("@").append(connector.hashCode()).append("\n"); - sb.append(" \n"); - for (String protocol : connector.getProtocols()) - { - sb.append(" ").append(protocol).append("\n"); - } - sb.append(" \n"); + Map top = new HashMap<>(); + + // requests + Map requests = new HashMap<>(); + requests.put("statsOnMs", _statsHandler.getStatsOnMs()); + + requests.put("requests", _statsHandler.getRequests()); + + requests.put("requestsActive", _statsHandler.getRequestsActive()); + requests.put("requestsActiveMax", _statsHandler.getRequestsActiveMax()); + requests.put("requestsTimeTotal", _statsHandler.getRequestTimeTotal()); + requests.put("requestsTimeMean", _statsHandler.getRequestTimeMean()); + requests.put("requestsTimeMax", _statsHandler.getRequestTimeMax()); + requests.put("requestsTimeStdDev", _statsHandler.getRequestTimeStdDev()); + + requests.put("dispatched", _statsHandler.getDispatched()); + requests.put("dispatchedActive", _statsHandler.getDispatchedActive()); + requests.put("dispatchedActiveMax", _statsHandler.getDispatchedActiveMax()); + requests.put("dispatchedTimeTotal", _statsHandler.getDispatchedTimeTotal()); + requests.put("dispatchedTimeMean", _statsHandler.getDispatchedTimeMean()); + requests.put("dispatchedTimeMax", _statsHandler.getDispatchedTimeMax()); + requests.put("dispatchedTimeStdDev", _statsHandler.getDispatchedTimeStdDev()); + + requests.put("asyncRequests", _statsHandler.getAsyncRequests()); + requests.put("requestsSuspended", _statsHandler.getAsyncDispatches()); + requests.put("requestsSuspendedMax", _statsHandler.getAsyncRequestsWaiting()); + requests.put("requestsResumed", _statsHandler.getAsyncRequestsWaitingMax()); + requests.put("requestsExpired", _statsHandler.getExpires()); + + requests.put("errors", _statsHandler.getErrors()); + + top.put("requests", requests); + + // responses + Map responses = new HashMap<>(); + responses.put("responses1xx", _statsHandler.getResponses1xx()); + responses.put("responses2xx", _statsHandler.getResponses2xx()); + responses.put("responses3xx", _statsHandler.getResponses3xx()); + responses.put("responses4xx", _statsHandler.getResponses4xx()); + responses.put("responses5xx", _statsHandler.getResponses5xx()); + responses.put("responsesBytesTotal", _statsHandler.getResponsesBytesTotal()); + top.put("responses", responses); + + // connections + List connections = new ArrayList<>(); + _connectors.forEach((connector) -> + { + Map connectorDetail = new HashMap<>(); + connectorDetail.put("name", String.format("%s@%X", connector.getClass().getName(), connector.hashCode())); + connectorDetail.put("protocols", connector.getProtocols()); ConnectionStatistics connectionStats = null; if (connector instanceof AbstractConnector) connectionStats = connector.getBean(ConnectionStatistics.class); if (connectionStats != null) { - sb.append(" true\n"); - sb.append(" ").append(connectionStats.getConnectionsTotal()).append("\n"); - sb.append(" ").append(connectionStats.getConnections()).append("\n"); - sb.append(" ").append(connectionStats.getConnectionsMax()).append("\n"); - sb.append(" ").append(connectionStats.getConnectionDurationMean()).append("\n"); - sb.append(" ").append(connectionStats.getConnectionDurationMax()).append("\n"); - sb.append(" ").append(connectionStats.getConnectionDurationStdDev()).append("\n"); - sb.append(" ").append(connectionStats.getReceivedBytes()).append("\n"); - sb.append(" ").append(connectionStats.getSentBytes()).append("\n"); - sb.append(" ").append(connectionStats.getReceivedMessages()).append("\n"); - sb.append(" ").append(connectionStats.getSentMessages()).append("\n"); + connectorDetail.put("statsOn", true); + connectorDetail.put("connections", connectionStats.getConnectionsTotal()); + connectorDetail.put("connectionsOpen>", connectionStats.getConnections()); + connectorDetail.put("connectionsOpenMax", connectionStats.getConnectionsMax()); + connectorDetail.put("connectionsDurationMean", connectionStats.getConnectionDurationMean()); + connectorDetail.put("connectionsDurationMax", connectionStats.getConnectionDurationMax()); + connectorDetail.put("connectionsDurationStdDev", connectionStats.getConnectionDurationStdDev()); + connectorDetail.put("bytesIn", connectionStats.getReceivedBytes()); + connectorDetail.put("bytesOut", connectionStats.getSentBytes()); + connectorDetail.put("messagesIn", connectionStats.getReceivedMessages()); + connectorDetail.put("messagesOut", connectionStats.getSentMessages()); } else { + // Support for deprecated ConnectorStatistics (will be dropped in Jetty 10+) ConnectorStatistics connectorStats = null; if (connector instanceof AbstractConnector) connectorStats = connector.getBean(ConnectorStatistics.class); if (connectorStats != null) { - sb.append(" true\n"); - sb.append(" ").append(connectorStats.getConnections()).append("\n"); - sb.append(" ").append(connectorStats.getConnectionsOpen()).append("\n"); - sb.append(" ").append(connectorStats.getConnectionsOpenMax()).append("\n"); - sb.append(" ").append(connectorStats.getConnectionDurationMean()).append("\n"); - sb.append(" ").append(connectorStats.getConnectionDurationMax()).append("\n"); - sb.append(" ").append(connectorStats.getConnectionDurationStdDev()).append("\n"); - sb.append(" ").append(connectorStats.getMessagesIn()).append("\n"); - sb.append(" ").append(connectorStats.getMessagesIn()).append("\n"); - sb.append(" ").append(connectorStats.getStartedMillis()).append("\n"); + connectorDetail.put("statsOn", true); + connectorDetail.put("connections", connectorStats.getConnections()); + connectorDetail.put("connectionsOpen", connectorStats.getConnectionsOpen()); + connectorDetail.put("connectionsOpenMax", connectorStats.getConnectionsOpenMax()); + connectorDetail.put("connectionsDurationMean", connectorStats.getConnectionDurationMean()); + connectorDetail.put("connectionsDurationMax", connectorStats.getConnectionDurationMax()); + connectorDetail.put("connectionsDurationStdDev", connectorStats.getConnectionDurationStdDev()); + connectorDetail.put("messagesIn", connectorStats.getMessagesIn()); + connectorDetail.put("messagesOut", connectorStats.getMessagesIn()); + connectorDetail.put("elapsedMs", connectorStats.getStartedMillis()); } else { - sb.append(" false\n"); + connectorDetail.put("statsOn", false); } } - sb.append(" \n"); + connections.add(connectorDetail); + }); + top.put("connections", connections); + + // memory + Map memoryMap = new HashMap<>(); + memoryMap.put("heapMemoryUsage", _memoryBean.getHeapMemoryUsage().getUsed()); + memoryMap.put("nonHeapMemoryUsage", _memoryBean.getNonHeapMemoryUsage().getUsed()); + top.put("memory", memoryMap); + + // the top level object + return outputProducer.generate("statistics", top); + } + + private interface OutputProducer + { + CharSequence generate(String id, Map map); + } + + private static class JsonProducer implements OutputProducer + { + @Override + public CharSequence generate(String id, Map map) + { + return JSON.toString(map); + } + } + + private static class XmlProducer implements OutputProducer + { + private final StringBuilder sb; + private int indent = 0; + + public XmlProducer() + { + this.sb = new StringBuilder(); + } + + @Override + public CharSequence generate(String id, Map map) + { + add(id, map); + return sb; + } + + private void indent() + { + sb.append("\n"); + for (int i = 0; i < indent; i++) + { + sb.append(' ').append(' '); + } } - sb.append(" \n"); - sb.append(" \n"); - sb.append(" ").append(_memoryBean.getHeapMemoryUsage().getUsed()).append("\n"); - sb.append(" ").append(_memoryBean.getNonHeapMemoryUsage().getUsed()).append("\n"); - sb.append(" \n"); + private void add(String id, Object obj) + { + sb.append('<').append(StringUtil.sanitizeXmlString(id)).append('>'); + indent++; - sb.append("\n"); + boolean wasIndented = false; - response.setContentType("text/xml"); - PrintWriter pout = response.getWriter(); - pout.write(sb.toString()); + if (obj instanceof Map) + { + //noinspection unchecked + addMap((Map)obj); + wasIndented = true; + } + else if (obj instanceof List) + { + addList(id, (List)obj); + wasIndented = true; + } + else + { + addObject(obj); + } + + indent--; + if (wasIndented) + indent(); + sb.append("'); + } + + private void addMap(Map map) + { + map.keySet().stream().sorted() + .forEach((key) -> + { + indent(); + add(key, map.get(key)); + }); + } + + private void addList(String parentId, List list) + { + // drop the 's' at the end. + String childName = parentId.replaceFirst("s$", ""); + list.forEach((entry) -> + { + indent(); + add(childName, entry); + }); + } + + private void addObject(Object obj) + { + sb.append(StringUtil.sanitizeXmlString(Objects.toString(obj))); + } } - private void sendTextResponse(HttpServletResponse response) throws IOException + private static class TextProducer implements OutputProducer { - StringBuilder sb = new StringBuilder(); - sb.append(_statsHandler.toStatsHTML()); + private final StringBuilder sb; + private int indent = 0; + + public TextProducer() + { + this.sb = new StringBuilder(); + } + + @Override + public CharSequence generate(String id, Map map) + { + add(id, map); + return sb; + } - sb.append("

Connections:

\n"); - for (Connector connector : _connectors) + private void indent() { - sb.append("

").append(connector.getClass().getName()).append("@").append(connector.hashCode()).append("

"); - sb.append("Protocols:"); - for (String protocol : connector.getProtocols()) + for (int i = 0; i < indent; i++) { - sb.append(protocol).append(" "); + sb.append(' ').append(' '); } - sb.append("
\n"); + } - ConnectionStatistics connectionStats = null; - if (connector instanceof Container) - connectionStats = connector.getBean(ConnectionStatistics.class); - if (connectionStats != null) + private void add(String id, Object obj) + { + indent(); + sb.append(id).append(": "); + indent++; + + if (obj instanceof Map) + { + sb.append('\n'); + //noinspection unchecked + addMap((Map)obj); + } + else if (obj instanceof List) { - sb.append("Total connections: ").append(connectionStats.getConnectionsTotal()).append("
\n"); - sb.append("Current connections open: ").append(connectionStats.getConnections()).append("
\n"); - sb.append("Max concurrent connections open: ").append(connectionStats.getConnectionsMax()).append("
\n"); - sb.append("Mean connection duration: ").append(connectionStats.getConnectionDurationMean()).append("
\n"); - sb.append("Max connection duration: ").append(connectionStats.getConnectionDurationMax()).append("
\n"); - sb.append("Connection duration standard deviation: ").append(connectionStats.getConnectionDurationStdDev()).append("
\n"); - sb.append("Total bytes received: ").append(connectionStats.getReceivedBytes()).append("
\n"); - sb.append("Total bytes sent: ").append(connectionStats.getSentBytes()).append("
\n"); - sb.append("Total messages received: ").append(connectionStats.getReceivedMessages()).append("
\n"); - sb.append("Total messages sent: ").append(connectionStats.getSentMessages()).append("
\n"); + sb.append('\n'); + addList(id, (List)obj); } else { - ConnectorStatistics connectorStats = null; - if (connector instanceof AbstractConnector) - connectorStats = connector.getBean(ConnectorStatistics.class); - if (connectorStats != null) - { - sb.append("Statistics gathering started ").append(connectorStats.getStartedMillis()).append("ms ago").append("
\n"); - sb.append("Total connections: ").append(connectorStats.getConnections()).append("
\n"); - sb.append("Current connections open: ").append(connectorStats.getConnectionsOpen()).append("
\n"); - sb.append("Max concurrent connections open: ").append(connectorStats.getConnectionsOpenMax()).append("
\n"); - sb.append("Mean connection duration: ").append(connectorStats.getConnectionDurationMean()).append("
\n"); - sb.append("Max connection duration: ").append(connectorStats.getConnectionDurationMax()).append("
\n"); - sb.append("Connection duration standard deviation: ").append(connectorStats.getConnectionDurationStdDev()).append("
\n"); - sb.append("Total messages in: ").append(connectorStats.getMessagesIn()).append("
\n"); - sb.append("Total messages out: ").append(connectorStats.getMessagesOut()).append("
\n"); - } - else - { - sb.append("Statistics gathering off.\n"); - } + addObject(obj); + sb.append('\n'); } + + indent--; } - sb.append("

Memory:

\n"); - sb.append("Heap memory usage: ").append(_memoryBean.getHeapMemoryUsage().getUsed()).append(" bytes").append("
\n"); - sb.append("Non-heap memory usage: ").append(_memoryBean.getNonHeapMemoryUsage().getUsed()).append(" bytes").append("
\n"); + private void addMap(Map map) + { + map.keySet().stream().sorted() + .forEach((key) -> add(key, map.get(key))); + } - response.setContentType("text/html"); - PrintWriter pout = response.getWriter(); - pout.write(sb.toString()); + private void addList(String parentId, List list) + { + // drop the 's' at the end. + String childName = parentId.replaceFirst("s$", ""); + list.forEach((entry) -> add(childName, entry)); + } + + private void addObject(Object obj) + { + sb.append(obj); + } + } + + private static class HtmlProducer implements OutputProducer + { + private final StringBuilder sb; + private int indent = 0; + + public HtmlProducer() + { + this.sb = new StringBuilder(); + } + + @Override + public CharSequence generate(String id, Map map) + { + sb.append("
    \n"); + add(id, map); + sb.append("
\n"); + return sb; + } + + private void indent() + { + for (int i = 0; i < indent; i++) + { + sb.append(' ').append(' '); + } + } + + private void add(String id, Object obj) + { + indent(); + indent++; + sb.append("
  • ").append(StringUtil.sanitizeXmlString(id)).append(": "); + if (obj instanceof Map) + { + //noinspection unchecked + addMap((Map)obj); + indent(); + } + else if (obj instanceof List) + { + addList(id, (List)obj); + indent(); + } + else + { + addObject(obj); + } + sb.append("
  • \n"); + + indent--; + } + + private void addMap(Map map) + { + sb.append("\n"); + indent(); + sb.append("
      \n"); + indent++; + map.keySet().stream().sorted(String::compareToIgnoreCase) + .forEach((key) -> add(key, map.get(key))); + indent--; + indent(); + sb.append("
    \n"); + } + + private void addList(String parentId, List list) + { + sb.append("\n"); + indent(); + sb.append("
      \n"); + indent++; + // drop the 's' at the end. + String childName = parentId.replaceFirst("s$", ""); + list.forEach((entry) -> add(childName, entry)); + indent--; + indent(); + sb.append("
    \n"); + } + + private void addObject(Object obj) + { + sb.append(StringUtil.sanitizeXmlString(Objects.toString(obj))); + } } } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java index ef99d909e3bf..5590d9bd2850 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java @@ -18,31 +18,42 @@ package org.eclipse.jetty.servlet; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.PrintWriter; import java.io.StringReader; import java.nio.ByteBuffer; +import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathFactory; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.util.ajax.JSON; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; import org.xml.sax.InputSource; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class StatisticsServletTest { @@ -66,9 +77,7 @@ public void destroyServer() _server.join(); } - @Test - public void getStats() - throws Exception + private void addStatisticsHandler() { StatisticsHandler statsHandler = new StatisticsHandler(); _server.setHandler(statsHandler); @@ -78,30 +87,196 @@ public void getStats() servletHolder.setInitParameter("restrictToLocalhost", "false"); statsContext.addServlet(servletHolder, "/stats"); statsContext.setSessionHandler(new SessionHandler()); + } + + @Test + public void getStats() + throws Exception + { + addStatisticsHandler(); _server.start(); - getResponse("/test1"); - String response = getResponse("/stats?xml=true"); - Stats stats = parseStats(response); + HttpTester.Response response; + + // Trigger 2xx response + response = getResponse("/test1"); + assertEquals(response.getStatus(), 200); + + // Look for 200 response that was tracked + response = getResponse("/stats?xml=true"); + assertEquals(response.getStatus(), 200); + Stats stats = parseStats(response.getContent()); assertEquals(1, stats.responses2xx); - getResponse("/stats?statsReset=true"); + // Reset stats + response = getResponse("/stats?statsReset=true"); + assertEquals(response.getStatus(), 200); + + // Request stats again response = getResponse("/stats?xml=true"); - stats = parseStats(response); + assertEquals(response.getStatus(), 200); + stats = parseStats(response.getContent()); assertEquals(1, stats.responses2xx); - getResponse("/test1"); - getResponse("/nothing"); + // Trigger 2xx response + response = getResponse("/test1"); + assertEquals(response.getStatus(), 200); + // Trigger 4xx response + response = getResponse("/nothing"); + assertEquals(response.getStatus(), 404); + + // Request stats again response = getResponse("/stats?xml=true"); - stats = parseStats(response); + assertEquals(response.getStatus(), 200); + stats = parseStats(response.getContent()); + // Verify we see (from last reset) + // 1) request for /stats?statsReset=true [2xx] + // 2) request for /stats?xml=true [2xx] + // 3) request for /test1 [2xx] + // 4) request for /nothing [4xx] assertThat("2XX Response Count" + response, stats.responses2xx, is(3)); assertThat("4XX Response Count" + response, stats.responses4xx, is(1)); } - public String getResponse(String path) + @Test + public void getXmlResponse() + throws Exception + { + addStatisticsHandler(); + _server.start(); + + HttpTester.Response response; + HttpTester.Request request = new HttpTester.Request(); + + request.setMethod("GET"); + request.setURI("/stats"); + request.setHeader("Accept", "text/xml"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = _connector.getResponse(request.generate()); + response = HttpTester.parseResponse(responseBuffer); + + assertThat("Response.contentType", response.get(HttpHeader.CONTENT_TYPE), containsString("text/xml")); + + // System.out.println(response.getContent()); + + // Parse it, make sure it's well formed. + DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); + try (ByteArrayInputStream input = new ByteArrayInputStream(response.getContentBytes())) + { + Document doc = docBuilder.parse(input); + assertNotNull(doc); + assertEquals("statistics", doc.getDocumentElement().getNodeName()); + } + } + + @Test + public void getJsonResponse() + throws Exception + { + addStatisticsHandler(); + _server.start(); + + HttpTester.Response response; + HttpTester.Request request = new HttpTester.Request(); + + request.setMethod("GET"); + request.setURI("/stats"); + request.setHeader("Accept", "application/json"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = _connector.getResponse(request.generate()); + response = HttpTester.parseResponse(responseBuffer); + + assertThat("Response.contentType", response.get(HttpHeader.CONTENT_TYPE), is("application/json")); + assertThat("Response.contentType for json should never contain a charset", + response.get(HttpHeader.CONTENT_TYPE), not(containsString("charset"))); + + // System.out.println(response.getContent()); + + // Parse it, make sure it's well formed. + Object doc = JSON.parse(response.getContent()); + assertNotNull(doc); + assertThat(doc, instanceOf(Map.class)); + Map docMap = (Map)doc; + assertEquals(4, docMap.size()); + assertNotNull(docMap.get("requests")); + assertNotNull(docMap.get("responses")); + assertNotNull(docMap.get("connections")); + assertNotNull(docMap.get("memory")); + } + + @Test + public void getTextResponse() + throws Exception + { + addStatisticsHandler(); + _server.start(); + + HttpTester.Response response; + HttpTester.Request request = new HttpTester.Request(); + + request.setMethod("GET"); + request.setURI("/stats"); + request.setHeader("Accept", "text/plain"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = _connector.getResponse(request.generate()); + response = HttpTester.parseResponse(responseBuffer); + + assertThat("Response.contentType", response.get(HttpHeader.CONTENT_TYPE), containsString("text/plain")); + + // System.out.println(response.getContent()); + + // Look for expected content + assertThat(response.getContent(), containsString("requests: ")); + assertThat(response.getContent(), containsString("responses: ")); + assertThat(response.getContent(), containsString("connections: ")); + assertThat(response.getContent(), containsString("memory: ")); + } + + @Test + public void getHtmlResponse() + throws Exception + { + addStatisticsHandler(); + _server.start(); + + HttpTester.Response response; + HttpTester.Request request = new HttpTester.Request(); + + request.setMethod("GET"); + request.setURI("/stats"); + request.setHeader("Accept", "text/html"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = _connector.getResponse(request.generate()); + response = HttpTester.parseResponse(responseBuffer); + + assertThat("Response.contentType", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html")); + + System.out.println(response.getContent()); + + // Look for things that indicate it's a well formed HTML output + assertThat(response.getContent(), containsString("")); + assertThat(response.getContent(), containsString("")); + assertThat(response.getContent(), containsString("requests: ")); + assertThat(response.getContent(), containsString("responses: ")); + assertThat(response.getContent(), containsString("connections: ")); + assertThat(response.getContent(), containsString("memory: ")); + assertThat(response.getContent(), containsString("")); + assertThat(response.getContent(), containsString("")); + } + + public HttpTester.Response getResponse(String path) throws Exception { HttpTester.Request request = new HttpTester.Request(); @@ -111,7 +286,7 @@ public String getResponse(String path) request.setHeader("Host", "test"); ByteBuffer responseBuffer = _connector.getResponse(request.generate()); - return HttpTester.parseResponse(responseBuffer).getContent(); + return HttpTester.parseResponse(responseBuffer); } public Stats parseStats(String xml) @@ -120,7 +295,6 @@ public Stats parseStats(String xml) XPath xPath = XPathFactory.newInstance().newXPath(); String responses4xx = xPath.evaluate("//responses4xx", new InputSource(new StringReader(xml))); - String responses2xx = xPath.evaluate("//responses2xx", new InputSource(new StringReader(xml))); return new Stats(Integer.parseInt(responses2xx), Integer.parseInt(responses4xx));