From fa68733ab67e0ddb8d1717183fffcfb8e4219d64 Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Thu, 29 Sep 2022 16:54:41 +0200 Subject: [PATCH] fixes #155: Add statistics for endpoint responses This commit adds statistics for every response generated in response to a HTTP request on one of the REST API endpoints. The statistics show the amount of responses for each particular 'family' of status codes (1xx, 2xx, 3xx, 4xx and 5xx). The Monitoring plugin can be used to review graphs of this data. Note that the current latest release of the Monitoring plugin (2.3.1) has an issue that affects the functionality when a plugin that _provides_ a statistic is unloaded/reloaded. See https://github.com/igniterealtime/openfire-monitoring-plugin/issues/238 for details. It is recommended to use this commit only in combination with a version of the Monitoring plugin in which this issue is fixed. A work-around for the issue, if it does occur, is to restart Openfire after a plugin has been unloaded/reloaded. --- src/i18n/restapi_i18n.properties | 19 +++++ .../plugin/rest/RESTServicePlugin.java | 22 +++++- .../plugin/rest/StatisticsFilter.java | 79 +++++++++++++++++++ .../plugin/rest/service/JerseyWrapper.java | 2 + 4 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 src/java/org/jivesoftware/openfire/plugin/rest/StatisticsFilter.java diff --git a/src/i18n/restapi_i18n.properties b/src/i18n/restapi_i18n.properties index 64631ec5d..7d552b93e 100644 --- a/src/i18n/restapi_i18n.properties +++ b/src/i18n/restapi_i18n.properties @@ -1 +1,20 @@ system_property.plugin.restapi.muc.case-insensitive-lookup.enabled=Names of MUC rooms should be node-prepped. This, however, was not guaranteed the case in some versions of Openfire and this plugin. Earlier versions of this plugin used a case-insensitive lookup to work around this. As this should be unneeded, and is quite resource intensive, this behavior has been made configurable (disabled by default). + +stat.restapi_responses.informational.name=REST API 1xx responses +stat.restapi_responses.informational.desc=The amount of HTTP responses that had an 'Informational' status (a code in the 1xx range). +stat.restapi_responses.informational.units=Responses +stat.restapi_responses.successful.name=REST API 2xx responses +stat.restapi_responses.successful.desc=The amount of HTTP responses that had a 'Successful' status (a code in the 2xx range). +stat.restapi_responses.successful.units=Responses +stat.restapi_responses.redirection.name=REST API 3xx responses +stat.restapi_responses.redirection.desc=The amount of HTTP responses that had a 'Redirection' status (a code in the 3xx range). +stat.restapi_responses.redirection.units=Responses +stat.restapi_responses.client_error.name=REST API 4xx responses +stat.restapi_responses.client_error.desc=The amount of HTTP responses that had a 'Client Error' status (a code in the 4xx range). +stat.restapi_responses.client_error.units=Responses +stat.restapi_responses.server_error.name=REST API 5xx responses +stat.restapi_responses.server_error.desc=The amount of HTTP responses that had a 'Server Error' status (a code in the 5xx range). +stat.restapi_responses.server_error.units=Responses +stat.restapi_responses.other.name=REST API unknown responses +stat.restapi_responses.other.desc=The amount of HTTP responses that had an unrecognized status code. +stat.restapi_responses.other.units=Responses diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/RESTServicePlugin.java b/src/java/org/jivesoftware/openfire/plugin/rest/RESTServicePlugin.java index ab4194dfe..2f61a5692 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/RESTServicePlugin.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/RESTServicePlugin.java @@ -20,15 +20,14 @@ import org.jivesoftware.openfire.container.Plugin; import org.jivesoftware.openfire.container.PluginManager; import org.jivesoftware.openfire.plugin.rest.service.JerseyWrapper; +import org.jivesoftware.openfire.stats.StatisticsManager; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.PropertyEventDispatcher; import org.jivesoftware.util.PropertyEventListener; import org.jivesoftware.util.StringUtils; import java.io.File; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; +import java.util.*; /** * The Class RESTServicePlugin. @@ -64,6 +63,8 @@ public void setServiceLoggingEnabled(boolean serviceLoggingEnabled) { /** The custom authentication filter */ private String customAuthFilterClassName; + private final Set registeredStatisticKeys = new HashSet<>(); + /* (non-Javadoc) * @see org.jivesoftware.openfire.container.Plugin#initializePlugin(org.jivesoftware.openfire.container.PluginManager, java.io.File) */ @@ -77,7 +78,13 @@ public void initializePlugin(PluginManager manager, File pluginDirectory) { // See if Custom authentication filter has been defined customAuthFilterClassName = JiveGlobals.getProperty("plugin.restapi.customAuthFilter", ""); - + + // Start collecting statistics. + for (StatisticsFilter.RestResponseFamilyStatistic statistic : StatisticsFilter.generateAllFamilyStatisticInstances()) { + StatisticsManager.getInstance().addStatistic(statistic.getKeyName(), statistic); + registeredStatisticKeys.add(statistic.getKeyName()); + } + // See if the service is enabled or not. enabled = JiveGlobals.getBooleanProperty("plugin.restapi.enabled", false); @@ -100,6 +107,13 @@ public void initializePlugin(PluginManager manager, File pluginDirectory) { * @see org.jivesoftware.openfire.container.Plugin#destroyPlugin() */ public void destroyPlugin() { + // Stop registering statistics. + final Iterator iter = registeredStatisticKeys.iterator(); + while (iter.hasNext()) { + StatisticsManager.getInstance().removeStatistic(iter.next()); + iter.remove(); + } + // Release the excluded URL AuthCheckFilter.removeExclude(JerseyWrapper.SERVLET_URL); // Stop listening to system property events diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/StatisticsFilter.java b/src/java/org/jivesoftware/openfire/plugin/rest/StatisticsFilter.java new file mode 100644 index 000000000..c8388514d --- /dev/null +++ b/src/java/org/jivesoftware/openfire/plugin/rest/StatisticsFilter.java @@ -0,0 +1,79 @@ +package org.jivesoftware.openfire.plugin.rest; + +import org.jivesoftware.openfire.stats.i18nStatistic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class StatisticsFilter implements ContainerResponseFilter +{ + private static final Logger Log = LoggerFactory.getLogger(StatisticsFilter.class); + + private static final ConcurrentMap ratePerFamily = new ConcurrentHashMap<>(); + + private static ConcurrentMap getStatsCollection() { + return ratePerFamily; + } + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException + { + final Response.StatusType statusInfo = responseContext.getStatusInfo(); + if (statusInfo == null) { + Log.warn("Cannot record statistics for a response that contains no status info. Response context object: {}", responseContext); + } else { + StatisticsFilter.getStatsCollection().merge(statusInfo.getFamily(), 1L, Long::sum); + } + } + + public static Collection generateAllFamilyStatisticInstances() { + final Collection result = new HashSet<>(); + for (Response.Status.Family family : Response.Status.Family.values()) { + result.add(new RestResponseFamilyStatistic(family)); + } + return result; + } + + public static class RestResponseFamilyStatistic extends i18nStatistic + { + public static final String GROUP = "restapi_responses"; + + private final Response.Status.Family family; + + public RestResponseFamilyStatistic(@Nonnull final Response.Status.Family family) + { + super(GROUP + "." + family.toString().toLowerCase(), "restapi", Type.rate); + this.family = family; + } + + @Override + public double sample() + { + final Long oldValue = StatisticsFilter.getStatsCollection().replace(family, 0L); + return oldValue == null ? 0 : oldValue; + } + + @Override + public boolean isPartialSample() + { + return true; + } + + public String getGroupName() { + return GROUP; + } + + public String getKeyName() { + return family.toString().toLowerCase(); + } + } +} diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/service/JerseyWrapper.java b/src/java/org/jivesoftware/openfire/plugin/rest/service/JerseyWrapper.java index cf8dbc9af..c61174eb3 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/service/JerseyWrapper.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/service/JerseyWrapper.java @@ -20,6 +20,7 @@ import org.jivesoftware.openfire.plugin.rest.AuthFilter; import org.jivesoftware.openfire.plugin.rest.CORSFilter; import org.jivesoftware.openfire.plugin.rest.CustomJacksonMapperProvider; +import org.jivesoftware.openfire.plugin.rest.StatisticsFilter; import org.jivesoftware.openfire.plugin.rest.exceptions.RESTExceptionMapper; import org.jivesoftware.util.JiveGlobals; @@ -95,6 +96,7 @@ public JerseyWrapper(@Context ServletConfig servletConfig) { // Filters loadAuthenticationFilter(); register(CORSFilter.class); + register(StatisticsFilter.class); // Services registerClasses(