Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-11.0.x

This commit is contained in:
Joakim Erdfelt 2020-11-17 09:20:25 -06:00
commit d30d95c26c
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
10 changed files with 930 additions and 151 deletions

View File

@ -177,6 +177,7 @@ public class TestOSGiUtil
.start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-slf4j-impl").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-util").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-util-ajax").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-deploy").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-server").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-servlet").versionAsInProject().start());

View File

@ -8,6 +8,10 @@ server
[depend]
server
servlet
[lib]
lib/jetty-util-ajax-${jetty.version}.jar
[xml]
etc/jetty-stats.xml

View File

@ -17,6 +17,15 @@
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
@{argLine} ${jetty.surefire.argLine}
--add-modules org.eclipse.jetty.util.ajax
</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
@ -49,6 +58,12 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util-ajax</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jmx</artifactId>

View File

@ -27,6 +27,7 @@ module org.eclipse.jetty.servlet
// Only required if using StatisticsServlet.
requires static java.management;
requires static org.eclipse.jetty.util.ajax;
// Only required if using IntrospectorCleaner.
requires static java.desktop;
// Only required if using JMX.

View File

@ -19,28 +19,53 @@
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 jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpHeader;
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.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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Collect and report statistics about requests / responses / connections and more.
* <p>
* You can use normal HTTP content negotiation to ask for the statistics.
* Specify a request <code>Accept</code> header for one of the following formats:
* </p>
* <ul>
* <li><code>application/json</code></li>
* <li><code>text/xml</code></li>
* <li><code>text/html</code></li>
* <li><code>text/plain</code> - default if no <code>Accept</code> header specified</li>
* </ul>
*/
public class StatisticsServlet extends HttpServlet
{
private static final Logger LOG = LoggerFactory.getLogger(StatisticsServlet.class);
@ -48,7 +73,7 @@ public class StatisticsServlet extends HttpServlet
boolean _restrictToLocalhost = true; // defaults to true
private StatisticsHandler _statsHandler;
private MemoryMXBean _memoryBean;
private Connector[] _connectors;
private List<Connector> _connectors;
@Override
public void init() throws ServletException
@ -57,20 +82,16 @@ public class StatisticsServlet extends HttpServlet
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)
{
@ -79,47 +100,147 @@ public class StatisticsServlet extends HttpServlet
}
@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");
if (request.getParameter("xml") != null)
{
LOG.warn("'xml' parameter is deprecated, use 'Accept' request header instead");
}
if (Boolean.parseBoolean(wantXml))
List<String> acceptable = getOrderedAcceptableMimeTypes(request);
for (String mimeType : acceptable)
{
sendXmlResponse(resp);
switch (mimeType)
{
case "application/json":
writeJsonResponse(response);
return;
case "text/xml":
writeXmlResponse(response);
return;
case "text/html":
writeHtmlResponse(response);
return;
case "text/plain":
case "*/*":
writeTextResponse(response);
return;
default:
if (LOG.isDebugEnabled())
{
LOG.debug("Ignoring unrecognized mime-type {}", mimeType);
}
break;
}
}
else
// 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("<html><head><title>");
htmlWriter.append(this.getClass().getSimpleName());
htmlWriter.append("</title></head><body>\n");
CharSequence html = generateResponse(new HtmlProducer());
htmlWriter.append(html.toString());
htmlWriter.append("\n</body></html>\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());
}
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<String> getOrderedAcceptableMimeTypes(HttpServletRequest request)
{
QuotedQualityCSV values = new QuotedQualityCSV(QuotedQualityCSV.MOST_SPECIFIC_MIME_ORDERING);
// No accept header specified, try 'accept' parameter (for those clients that are
// so ancient that they cannot set the standard HTTP `Accept` header)
String acceptParameter = request.getParameter("accept");
if (acceptParameter != null)
{
sendTextResponse(resp);
values.addValue(acceptParameter);
}
Enumeration<String> enumAccept = request.getHeaders(HttpHeader.ACCEPT.toString());
if (enumAccept != null)
{
while (enumAccept.hasMoreElements())
{
String value = enumAccept.nextElement();
if (StringUtil.isNotBlank(value))
{
values.addValue(value);
}
}
}
if (values.isEmpty())
{
// return that we allow ALL mime types
return Collections.singletonList("*/*");
}
return values.getValues();
}
private boolean isLoopbackAddress(String address)
@ -136,140 +257,336 @@ public class StatisticsServlet extends HttpServlet
}
}
private void sendXmlResponse(HttpServletResponse response) throws IOException
private CharSequence generateResponse(OutputProducer outputProducer)
{
StringBuilder sb = new StringBuilder();
Map<String, Object> top = new HashMap<>();
sb.append("<statistics>\n");
// requests
Map<String, Number> requests = new HashMap<>();
requests.put("statsOnMs", _statsHandler.getStatsOnMs());
sb.append(" <requests>\n");
sb.append(" <statsOnMs>").append(_statsHandler.getStatsOnMs()).append("</statsOnMs>\n");
requests.put("requests", _statsHandler.getRequests());
sb.append(" <requests>").append(_statsHandler.getRequests()).append("</requests>\n");
sb.append(" <requestsActive>").append(_statsHandler.getRequestsActive()).append("</requestsActive>\n");
sb.append(" <requestsActiveMax>").append(_statsHandler.getRequestsActiveMax()).append("</requestsActiveMax>\n");
sb.append(" <requestsTimeTotal>").append(_statsHandler.getRequestTimeTotal()).append("</requestsTimeTotal>\n");
sb.append(" <requestsTimeMean>").append(_statsHandler.getRequestTimeMean()).append("</requestsTimeMean>\n");
sb.append(" <requestsTimeMax>").append(_statsHandler.getRequestTimeMax()).append("</requestsTimeMax>\n");
sb.append(" <requestsTimeStdDev>").append(_statsHandler.getRequestTimeStdDev()).append("</requestsTimeStdDev>\n");
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());
sb.append(" <dispatched>").append(_statsHandler.getDispatched()).append("</dispatched>\n");
sb.append(" <dispatchedActive>").append(_statsHandler.getDispatchedActive()).append("</dispatchedActive>\n");
sb.append(" <dispatchedActiveMax>").append(_statsHandler.getDispatchedActiveMax()).append("</dispatchedActiveMax>\n");
sb.append(" <dispatchedTimeTotalMs>").append(_statsHandler.getDispatchedTimeTotal()).append("</dispatchedTimeTotalMs>\n");
sb.append(" <dispatchedTimeMeanMs>").append(_statsHandler.getDispatchedTimeMean()).append("</dispatchedTimeMeanMs>\n");
sb.append(" <dispatchedTimeMaxMs>").append(_statsHandler.getDispatchedTimeMax()).append("</dispatchedTimeMaxMs>\n");
sb.append(" <dispatchedTimeStdDevMs>").append(_statsHandler.getDispatchedTimeStdDev()).append("</dispatchedTimeStdDevMs>\n");
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());
sb.append(" <asyncRequests>").append(_statsHandler.getAsyncRequests()).append("</asyncRequests>\n");
sb.append(" <requestsSuspended>").append(_statsHandler.getAsyncRequestsWaiting()).append("</requestsSuspended>\n");
sb.append(" <requestsSuspendedMax>").append(_statsHandler.getAsyncRequestsWaitingMax()).append("</requestsSuspendedMax>\n");
sb.append(" <requestsResumed>").append(_statsHandler.getAsyncDispatches()).append("</requestsResumed>\n");
sb.append(" <requestsExpired>").append(_statsHandler.getExpires()).append("</requestsExpired>\n");
sb.append(" </requests>\n");
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());
sb.append(" <responses>\n");
sb.append(" <responses1xx>").append(_statsHandler.getResponses1xx()).append("</responses1xx>\n");
sb.append(" <responses2xx>").append(_statsHandler.getResponses2xx()).append("</responses2xx>\n");
sb.append(" <responses3xx>").append(_statsHandler.getResponses3xx()).append("</responses3xx>\n");
sb.append(" <responses4xx>").append(_statsHandler.getResponses4xx()).append("</responses4xx>\n");
sb.append(" <responses5xx>").append(_statsHandler.getResponses5xx()).append("</responses5xx>\n");
sb.append(" <responsesBytesTotal>").append(_statsHandler.getResponsesBytesTotal()).append("</responsesBytesTotal>\n");
sb.append(" </responses>\n");
requests.put("errors", _statsHandler.getErrors());
sb.append(" <connections>\n");
for (Connector connector : _connectors)
top.put("requests", requests);
// responses
Map<String, Number> 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<Object> connections = new ArrayList<>();
_connectors.forEach((connector) ->
{
sb.append(" <connector>\n");
sb.append(" <name>").append(connector.getClass().getName()).append("@").append(connector.hashCode()).append("</name>\n");
sb.append(" <protocols>\n");
for (String protocol : connector.getProtocols())
{
sb.append(" <protocol>").append(protocol).append("</protocol>\n");
}
sb.append(" </protocols>\n");
Map<String, Object> 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 = ((AbstractConnector)connector).getBean(ConnectionStatistics.class);
ConnectionStatistics connectionStats = connector.getBean(ConnectionStatistics.class);
if (connectionStats != null)
{
sb.append(" <statsOn>true</statsOn>\n");
sb.append(" <connections>").append(connectionStats.getConnectionsTotal()).append("</connections>\n");
sb.append(" <connectionsOpen>").append(connectionStats.getConnections()).append("</connectionsOpen>\n");
sb.append(" <connectionsOpenMax>").append(connectionStats.getConnectionsMax()).append("</connectionsOpenMax>\n");
sb.append(" <connectionsDurationMean>").append(connectionStats.getConnectionDurationMean()).append("</connectionsDurationMean>\n");
sb.append(" <connectionsDurationMax>").append(connectionStats.getConnectionDurationMax()).append("</connectionsDurationMax>\n");
sb.append(" <connectionsDurationStdDev>").append(connectionStats.getConnectionDurationStdDev()).append("</connectionsDurationStdDev>\n");
sb.append(" <bytesIn>").append(connectionStats.getReceivedBytes()).append("</bytesIn>\n");
sb.append(" <bytesOut>").append(connectionStats.getSentBytes()).append("</bytesOut>\n");
sb.append(" <messagesIn>").append(connectionStats.getReceivedMessages()).append("</messagesIn>\n");
sb.append(" <messagesOut>").append(connectionStats.getSentMessages()).append("</messagesOut>\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
{
sb.append(" <statsOn>false</statsOn>\n");
}
sb.append(" </connector>\n");
}
sb.append(" </connections>\n");
connections.add(connectorDetail);
});
top.put("connections", connections);
sb.append(" <memory>\n");
sb.append(" <heapMemoryUsage>").append(_memoryBean.getHeapMemoryUsage().getUsed()).append("</heapMemoryUsage>\n");
sb.append(" <nonHeapMemoryUsage>").append(_memoryBean.getNonHeapMemoryUsage().getUsed()).append("</nonHeapMemoryUsage>\n");
sb.append(" </memory>\n");
// memory
Map<String, Number> memoryMap = new HashMap<>();
memoryMap.put("heapMemoryUsage", _memoryBean.getHeapMemoryUsage().getUsed());
memoryMap.put("nonHeapMemoryUsage", _memoryBean.getNonHeapMemoryUsage().getUsed());
top.put("memory", memoryMap);
sb.append("</statistics>\n");
response.setContentType("text/xml");
PrintWriter pout = response.getWriter();
pout.write(sb.toString());
// the top level object
return outputProducer.generate("statistics", top);
}
private void sendTextResponse(HttpServletResponse response) throws IOException
private interface OutputProducer
{
StringBuilder sb = new StringBuilder();
sb.append(_statsHandler.toStatsHTML());
CharSequence generate(String id, Map<String, Object> map);
}
sb.append("<h2>Connections:</h2>\n");
for (Connector connector : _connectors)
private static class JsonProducer implements OutputProducer
{
@Override
public CharSequence generate(String id, Map<String, Object> map)
{
sb.append("<h3>").append(connector.getClass().getName()).append("@").append(connector.hashCode()).append("</h3>");
sb.append("Protocols:");
for (String protocol : connector.getProtocols())
{
sb.append(protocol).append("&nbsp;");
}
sb.append(" <br />\n");
return new JSON().toJSON(map);
}
}
ConnectionStatistics connectionStats = null;
if (connector instanceof Container)
connectionStats = ((Container)connector).getBean(ConnectionStatistics.class);
if (connectionStats != null)
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<String, Object> map)
{
add(id, map);
return sb;
}
private void indent()
{
sb.append("\n");
for (int i = 0; i < indent; i++)
{
sb.append("Total connections: ").append(connectionStats.getConnectionsTotal()).append("<br />\n");
sb.append("Current connections open: ").append(connectionStats.getConnections()).append("<br />\n");
sb.append("Max concurrent connections open: ").append(connectionStats.getConnectionsMax()).append("<br />\n");
sb.append("Mean connection duration: ").append(connectionStats.getConnectionDurationMean()).append("<br />\n");
sb.append("Max connection duration: ").append(connectionStats.getConnectionDurationMax()).append("<br />\n");
sb.append("Connection duration standard deviation: ").append(connectionStats.getConnectionDurationStdDev()).append("<br />\n");
sb.append("Total bytes received: ").append(connectionStats.getReceivedBytes()).append("<br />\n");
sb.append("Total bytes sent: ").append(connectionStats.getSentBytes()).append("<br />\n");
sb.append("Total messages received: ").append(connectionStats.getReceivedMessages()).append("<br />\n");
sb.append("Total messages sent: ").append(connectionStats.getSentMessages()).append("<br />\n");
}
else
{
sb.append("Statistics gathering off.\n");
sb.append(' ').append(' ');
}
}
sb.append("<h2>Memory:</h2>\n");
sb.append("Heap memory usage: ").append(_memoryBean.getHeapMemoryUsage().getUsed()).append(" bytes").append("<br />\n");
sb.append("Non-heap memory usage: ").append(_memoryBean.getNonHeapMemoryUsage().getUsed()).append(" bytes").append("<br />\n");
private void add(String id, Object obj)
{
sb.append('<').append(StringUtil.sanitizeXmlString(id)).append('>');
indent++;
response.setContentType("text/html");
PrintWriter pout = response.getWriter();
pout.write(sb.toString());
boolean wasIndented = false;
if (obj instanceof Map)
{
//noinspection unchecked
addMap((Map<String, ?>)obj);
wasIndented = true;
}
else if (obj instanceof List)
{
addList(id, (List<?>)obj);
wasIndented = true;
}
else
{
addObject(obj);
}
indent--;
if (wasIndented)
indent();
sb.append("</").append(id).append('>');
}
private void addMap(Map<String, ?> 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 static class TextProducer implements OutputProducer
{
private final StringBuilder sb;
private int indent = 0;
public TextProducer()
{
this.sb = new StringBuilder();
}
@Override
public CharSequence generate(String id, Map<String, Object> map)
{
add(id, map);
return sb;
}
private void indent()
{
for (int i = 0; i < indent; i++)
{
sb.append(' ').append(' ');
}
}
private void add(String id, Object obj)
{
indent();
sb.append(id).append(": ");
indent++;
if (obj instanceof Map)
{
sb.append('\n');
//noinspection unchecked
addMap((Map<String, ?>)obj);
}
else if (obj instanceof List)
{
sb.append('\n');
addList(id, (List<?>)obj);
}
else
{
addObject(obj);
sb.append('\n');
}
indent--;
}
private void addMap(Map<String, ?> map)
{
map.keySet().stream().sorted()
.forEach((key) -> 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) -> 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<String, Object> map)
{
sb.append("<ul>\n");
add(id, map);
sb.append("</ul>\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("<li><em>").append(StringUtil.sanitizeXmlString(id)).append("</em>: ");
if (obj instanceof Map)
{
//noinspection unchecked
addMap((Map<String, ?>)obj);
indent();
}
else if (obj instanceof List)
{
addList(id, (List<?>)obj);
indent();
}
else
{
addObject(obj);
}
sb.append("</li>\n");
indent--;
}
private void addMap(Map<String, ?> map)
{
sb.append("\n");
indent();
sb.append("<ul>\n");
indent++;
map.keySet().stream().sorted(String::compareToIgnoreCase)
.forEach((key) -> add(key, map.get(key)));
indent--;
indent();
sb.append("</ul>\n");
}
private void addList(String parentId, List<?> list)
{
sb.append("\n");
indent();
sb.append("<ul>\n");
indent++;
// drop the 's' at the end.
String childName = parentId.replaceFirst("s$", "");
list.forEach((entry) -> add(childName, entry));
indent--;
indent();
sb.append("</ul>\n");
}
private void addObject(Object obj)
{
sb.append(StringUtil.sanitizeXmlString(Objects.toString(obj)));
}
}
}

View File

@ -18,10 +18,16 @@
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 java.util.function.Consumer;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;
@ -29,20 +35,30 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
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 +82,7 @@ public class StatisticsServletTest
_server.join();
}
@Test
public void getStats()
throws Exception
private void addStatisticsHandler()
{
StatisticsHandler statsHandler = new StatisticsHandler();
_server.setHandler(statsHandler);
@ -78,40 +92,267 @@ public class StatisticsServletTest
servletHolder.setInitParameter("restrictToLocalhost", "false");
statsContext.addServlet(servletHolder, "/stats");
statsContext.setSessionHandler(new SessionHandler());
}
@Test
public void testGetStats()
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");
assertEquals(response.getStatus(), 200);
Stats stats = parseStats(response.getContent());
assertEquals(1, stats.responses2xx);
getResponse("/stats?statsReset=true");
response = getResponse("/stats?xml=true");
stats = parseStats(response);
// Reset stats
response = getResponse("/stats?statsReset=true");
assertEquals(response.getStatus(), 200);
// Request stats again
response = getResponse("/stats");
assertEquals(response.getStatus(), 200);
stats = parseStats(response.getContent());
assertEquals(1, stats.responses2xx);
getResponse("/test1");
getResponse("/nothing");
response = getResponse("/stats?xml=true");
stats = parseStats(response);
// 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");
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)
public static Stream<Arguments> typeVariations(String mimeType)
{
return Stream.of(
Arguments.of(
new Consumer<HttpTester.Request>()
{
@Override
public void accept(HttpTester.Request request)
{
request.setURI("/stats");
request.setHeader("Accept", mimeType);
}
@Override
public String toString()
{
return "Header[Accept: " + mimeType + "]";
}
}
),
Arguments.of(
new Consumer<HttpTester.Request>()
{
@Override
public void accept(HttpTester.Request request)
{
request.setURI("/stats?accept=" + mimeType);
}
@Override
public String toString()
{
return "query[accept=" + mimeType + "]";
}
}
)
);
}
public static Stream<Arguments> xmlVariations()
{
return typeVariations("text/xml");
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("xmlVariations")
public void testGetXmlResponse(Consumer<HttpTester.Request> requestCustomizer)
throws Exception
{
addStatisticsHandler();
_server.start();
HttpTester.Response response;
HttpTester.Request request = new HttpTester.Request();
request.setMethod("GET");
request.setVersion(HttpVersion.HTTP_1_1);
request.setHeader("Host", "test");
requestCustomizer.accept(request);
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();
docBuilderFactory.setValidating(false);
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
try (ByteArrayInputStream input = new ByteArrayInputStream(response.getContentBytes()))
{
Document doc = docBuilder.parse(input);
assertNotNull(doc);
assertEquals("statistics", doc.getDocumentElement().getNodeName());
}
}
public static Stream<Arguments> jsonVariations()
{
return typeVariations("application/json");
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("jsonVariations")
public void testGetJsonResponse(Consumer<HttpTester.Request> requestCustomizer)
throws Exception
{
addStatisticsHandler();
_server.start();
HttpTester.Response response;
HttpTester.Request request = new HttpTester.Request();
request.setMethod("GET");
requestCustomizer.accept(request);
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 = new JSON().parse(new JSON.StringSource(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"));
}
public static Stream<Arguments> plaintextVariations()
{
return typeVariations("text/plain");
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("plaintextVariations")
public void testGetTextResponse(Consumer<HttpTester.Request> requestCustomizer)
throws Exception
{
addStatisticsHandler();
_server.start();
HttpTester.Response response;
HttpTester.Request request = new HttpTester.Request();
request.setMethod("GET");
requestCustomizer.accept(request);
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: "));
}
public static Stream<Arguments> htmlVariations()
{
return typeVariations("text/html");
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("htmlVariations")
public void testGetHtmlResponse(Consumer<HttpTester.Request> requestCustomizer)
throws Exception
{
addStatisticsHandler();
_server.start();
HttpTester.Response response;
HttpTester.Request request = new HttpTester.Request();
request.setMethod("GET");
requestCustomizer.accept(request);
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("<html>"));
assertThat(response.getContent(), containsString("<body>"));
assertThat(response.getContent(), containsString("<em>requests</em>: "));
assertThat(response.getContent(), containsString("<em>responses</em>: "));
assertThat(response.getContent(), containsString("<em>connections</em>: "));
assertThat(response.getContent(), containsString("<em>memory</em>: "));
assertThat(response.getContent(), containsString("</body>"));
assertThat(response.getContent(), containsString("</html>"));
}
public HttpTester.Response getResponse(String path)
throws Exception
{
HttpTester.Request request = new HttpTester.Request();
request.setMethod("GET");
request.setHeader("Accept", "text/xml");
request.setURI(path);
request.setVersion(HttpVersion.HTTP_1_1);
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 +361,6 @@ public class StatisticsServletTest
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));

View File

@ -134,6 +134,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util-ajax</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-jetty-api</artifactId>

View File

@ -0,0 +1,170 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.tests.distribution;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.util.ajax.JSON;
import org.junit.jupiter.api.Test;
import org.w3c.dom.Document;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class StatsTests extends AbstractJettyHomeTest
{
@Test
public void testStatsServlet() throws Exception
{
String jettyVersion = System.getProperty("jettyVersion");
JettyHomeTester distribution = JettyHomeTester.Builder.newInstance()
.jettyVersion(jettyVersion)
.mavenLocalRepository(System.getProperty("mavenRepoPath"))
.build();
String[] args1 = {
"--create-startd",
"--approve-all-licenses",
"--add-to-start=resources,server,http,webapp,deploy,stats"
};
try (JettyHomeTester.Run run1 = distribution.start(args1))
{
assertTrue(run1.awaitFor(5, TimeUnit.SECONDS));
assertEquals(0, run1.getExitValue());
Path webappsDir = distribution.getJettyBase().resolve("webapps");
FS.ensureDirExists(webappsDir.resolve("demo"));
FS.ensureDirExists(webappsDir.resolve("demo/WEB-INF"));
distribution.installBaseResource("stats-webapp/index.html", "webapps/demo/index.html");
distribution.installBaseResource("stats-webapp/WEB-INF/web.xml", "webapps/demo/WEB-INF/web.xml");
int port = distribution.freePort();
String[] args2 = {
"jetty.http.port=" + port
};
try (JettyHomeTester.Run run2 = distribution.start(args2))
{
assertTrue(run2.awaitConsoleLogsFor("Started Server@", 10, TimeUnit.SECONDS));
startHttpClient();
ContentResponse response;
URI serverBaseURI = URI.create("http://localhost:" + port);
response = client.GET(serverBaseURI.resolve("/demo/index.html"));
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat(response.getContentAsString(), containsString("<h1>Stats Demo</h1>"));
// ---------------
// Test XML accept
response = client.newRequest(serverBaseURI.resolve("/demo/stats"))
.method(HttpMethod.GET)
.headers((headers) -> headers.add(HttpHeader.ACCEPT, "text/xml"))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat("Response.contentType", response.getHeaders().get(HttpHeader.CONTENT_TYPE), containsString("text/xml"));
// Parse it, make sure it's well formed.
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
docBuilderFactory.setValidating(false);
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
try (ByteArrayInputStream input = new ByteArrayInputStream(response.getContent()))
{
Document doc = docBuilder.parse(input);
assertNotNull(doc);
assertEquals("statistics", doc.getDocumentElement().getNodeName());
}
// ---------------
// Test JSON accept
response = client.newRequest(serverBaseURI.resolve("/demo/stats"))
.method(HttpMethod.GET)
.headers((headers) -> headers.add(HttpHeader.ACCEPT, "application/json"))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat("Response.contentType", response.getHeaders().get(HttpHeader.CONTENT_TYPE), containsString("application/json"));
Object doc = new JSON().parse(new JSON.StringSource(response.getContentAsString()));
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 TEXT accept
response = client.newRequest(serverBaseURI.resolve("/demo/stats"))
.method(HttpMethod.GET)
.headers((headers) -> headers.add(HttpHeader.ACCEPT, "text/plain"))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat("Response.contentType", response.getHeaders().get(HttpHeader.CONTENT_TYPE), containsString("text/plain"));
String textContent = response.getContentAsString();
assertThat(textContent, containsString("requests: "));
assertThat(textContent, containsString("responses: "));
assertThat(textContent, containsString("connections: "));
assertThat(textContent, containsString("memory: "));
// ---------------
// Test HTML accept
response = client.newRequest(serverBaseURI.resolve("/demo/stats"))
.method(HttpMethod.GET)
.headers((headers) -> headers.add(HttpHeader.ACCEPT, "text/html"))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat("Response.contentType", response.getHeaders().get(HttpHeader.CONTENT_TYPE), containsString("text/html"));
String htmlContent = response.getContentAsString();
// Look for things that indicate it's a well formed HTML output
assertThat(htmlContent, containsString("<html>"));
assertThat(htmlContent, containsString("<body>"));
assertThat(htmlContent, containsString("<em>requests</em>: "));
assertThat(htmlContent, containsString("<em>responses</em>: "));
assertThat(htmlContent, containsString("<em>connections</em>: "));
assertThat(htmlContent, containsString("<em>memory</em>: "));
assertThat(htmlContent, containsString("</body>"));
assertThat(htmlContent, containsString("</html>"));
}
}
}
}

View File

@ -0,0 +1,17 @@
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>stats-demo</display-name>
<servlet>
<servlet-name>Stats</servlet-name>
<servlet-class>org.eclipse.jetty.servlet.StatisticsServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Stats</servlet-name>
<url-pattern>/stats</url-pattern>
</servlet-mapping>
</web-app>

View File

@ -0,0 +1,8 @@
<html>
<head>
<title>stats-demo</title>
</head>
<body>
<h1>Stats Demo</h1>
</body>
</html>