Skip to content

Instantly share code, notes, and snippets.

@mdindoffer
Created February 5, 2019 12:28
Show Gist options
  • Save mdindoffer/21d61433e1cd2b3c23c5880c76a11713 to your computer and use it in GitHub Desktop.
Save mdindoffer/21d61433e1cd2b3c23c5880c76a11713 to your computer and use it in GitHub Desktop.
Elastic APM servlet filter for Wicket AJAX requests
package eu.dindoffer.wicket.filter;
import co.elastic.apm.api.ElasticApm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.regex.Pattern;
/**
* Intercepts Wicket AJAX requests to set valid names of Elastic APM transactions.
*/
public class AjaxAPMFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(AjaxAPMFilter.class);
private static final Pattern RESOURCE_ID_PATTERN = Pattern.compile("/\\d+");
private static final char APM_PATH_DELIMITER = ':';
private static final char WICKET_COMPONENT_DELIMITER = '-';
private static final char QUERY_PARAM_DELIMITER = '&';
private static final char WICKET_INTERFACE_PREFIX = 'I';
private static final char TILDE_CHAR = '~';
private static final char UNDERSCORE_CHAR = '_';
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String query = httpRequest.getQueryString();
if ((query != null) && containsWicketListener(query)) {
String resourcePath = removeFirstChar(stripResourceIDs(extractResourcePath(httpRequest)));
String wicketIdentifier = encodeTildesForKibanaWorkaround(stripParentComponents(extractWicketComponentPath(query)));
String transactionName = resourcePath + APM_PATH_DELIMITER + wicketIdentifier;
LOG.trace("AJAX transaction name: {}", transactionName);
ElasticApm.currentTransaction().setName(transactionName);
}
chain.doFilter(request, response);
}
/**
* Checks whether the query string mentions one of Wicket AJAX listeners.
*
* @param queryString a query string
* @return true if the string contains an AJAX listener, false otherwise
*/
private static boolean containsWicketListener(String queryString) {
return queryString.contains("IResourceListener")
|| queryString.contains("IBehaviorListener")
|| queryString.contains("ILinkListener")
|| queryString.contains("IOnChangeListener")
|| queryString.contains("IFormSubmitListener");
}
/**
* Extracts a "Resource" path from an http request. Resource path consists of servletPath and pathInfo,
* i.e. omits the contextPath, since that would not provide much relevance.
*
* @param httpRequest HTTP request to process
* @return extracted resource path
*/
private static String extractResourcePath(HttpServletRequest httpRequest) {
String pathInfo = httpRequest.getPathInfo();
return (pathInfo == null) ? httpRequest.getServletPath() : (httpRequest.getServletPath() + pathInfo);
}
/**
* Removes numeric resource identifiers from a resource path / URL.
*
* @param resourcePath a resource path to process
* @return a resource path without resource identifiers
*/
private static String stripResourceIDs(String resourcePath) {
return RESOURCE_ID_PATTERN.matcher(resourcePath).replaceAll("");
}
/**
* Removes a first char from a string.
*
* @param request string to remove from
* @return string without a first char
*/
private static String removeFirstChar(String request) {
return request.substring(1);
}
/**
* Extracts a wicket component path parameter pointing to target AJAX component from the HTTP query.
*
* @param query query string to process
* @return component path parameter
*/
private static String extractWicketComponentPath(String query) {
int firstAmpIndex = query.indexOf(QUERY_PARAM_DELIMITER);
int firstIIndex = query.indexOf(WICKET_INTERFACE_PREFIX);
return (firstAmpIndex > 0) ? query.substring(firstIIndex, firstAmpIndex) : query.substring(firstIIndex);
}
/**
* Removes Wicket parent hierarchy from a component path and leaves only the listener and target component ID.
*
* @param componentPath Wicket component path to process
* @return Wicket AJAX listener identifier without parent hierarchy
*/
private static String stripParentComponents(String componentPath) {
int firstDashIndex = componentPath.indexOf(WICKET_COMPONENT_DELIMITER);
int lastDashIndex = componentPath.lastIndexOf(WICKET_COMPONENT_DELIMITER);
return componentPath.substring(0, firstDashIndex) + componentPath.substring(lastDashIndex);
}
/**
* Replace all tildes in wicket component identifier with underscores. This is a workaround for Kibana's special
* handling of the tilde character, that gets replaced by the '%' sign in javascript, resulting in an invalid URI.
*
* @param wicketIdentifier wicket component identifier
* @return encoded component identifier
*/
private static String encodeTildesForKibanaWorkaround(String wicketIdentifier) {
return wicketIdentifier.replace(TILDE_CHAR, UNDERSCORE_CHAR);
}
@Override
public void destroy() {
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment