package org.stagemonitor.web.monitor.filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.stagemonitor.configuration.ConfigurationRegistry; import org.stagemonitor.core.CorePlugin; import org.stagemonitor.core.MeasurementSession; import org.stagemonitor.core.Stagemonitor; import org.stagemonitor.tracing.RequestMonitor; import org.stagemonitor.tracing.SpanContextInformation; import org.stagemonitor.tracing.TracingPlugin; import org.stagemonitor.util.StringUtils; import org.stagemonitor.web.WebPlugin; import org.stagemonitor.web.monitor.DefaultMonitoredHttpRequestFactory; import org.stagemonitor.web.monitor.MonitoredHttpRequest; import org.stagemonitor.web.monitor.MonitoredHttpRequestFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.ServiceLoader; import java.util.concurrent.atomic.AtomicBoolean; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import static javax.servlet.DispatcherType.REQUEST; public class HttpRequestMonitorFilter extends AbstractExclusionFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(HttpRequestMonitorFilter.class); protected final ConfigurationRegistry configuration; protected final CorePlugin corePlugin; protected final WebPlugin webPlugin; protected final RequestMonitor requestMonitor; private final List<HtmlInjector> htmlInjectors = new ArrayList<HtmlInjector>(); private boolean atLeastServletApi3 = false; private final MonitoredHttpRequestFactory monitoredHttpRequestFactory; private final AtomicBoolean firstRequest = new AtomicBoolean(true); public HttpRequestMonitorFilter() { this(Stagemonitor.getConfiguration()); } public HttpRequestMonitorFilter(ConfigurationRegistry configuration) { super(configuration.getConfig(WebPlugin.class).getExcludedRequestPaths()); logger.debug("Instantiating HttpRequestMonitorFilter"); this.configuration = configuration; this.webPlugin = configuration.getConfig(WebPlugin.class); this.corePlugin = configuration.getConfig(CorePlugin.class); this.requestMonitor = configuration.getConfig(TracingPlugin.class).getRequestMonitor(); final Iterator<MonitoredHttpRequestFactory> requestFactoryIterator = ServiceLoader.load(MonitoredHttpRequestFactory.class).iterator(); if (!requestFactoryIterator.hasNext()) { this.monitoredHttpRequestFactory = new DefaultMonitoredHttpRequestFactory(); } else { this.monitoredHttpRequestFactory = requestFactoryIterator.next(); } } @Override public void initInternal(FilterConfig filterConfig) throws ServletException { final MeasurementSession measurementSession = new MeasurementSession(getApplicationName(filterConfig), corePlugin.getHostName(), corePlugin.getInstanceName()); Stagemonitor.startMonitoring(measurementSession); final ServletContext servletContext = filterConfig.getServletContext(); atLeastServletApi3 = servletContext.getMajorVersion() >= 3; for (HtmlInjector htmlInjector : ServiceLoader.load(HtmlInjector.class)) { htmlInjector.init(new HtmlInjector.InitArguments(configuration, servletContext)); htmlInjectors.add(htmlInjector); } } private String getApplicationName(FilterConfig filterConfig) { String name = corePlugin.getApplicationName(); if (StringUtils.isEmpty(name)) { name = filterConfig.getServletContext().getServletContextName(); } if (StringUtils.isEmpty(name)) { name = CorePlugin.DEFAULT_APPLICATION_NAME; } return name; } @Override public final void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws IOException, ServletException { if (firstRequest.getAndSet(false) && Stagemonitor.getMeasurementSession().getInstanceName() == null) { setInstanceAndStartMonitoring(request.getServerName()); } if (corePlugin.isStagemonitorActive() && !isInternalRequest(request) && request.getDispatcherType() == REQUEST) { doMonitor(request, response, filterChain); } else { filterChain.doFilter(request, response); } } private synchronized void setInstanceAndStartMonitoring(String instanceName) { final MeasurementSession measurementSession = Stagemonitor.getMeasurementSession(); if (measurementSession.getInstanceName() == null) { MeasurementSession session = new MeasurementSession(measurementSession.getApplicationName(), measurementSession.getHostName(), instanceName); Stagemonitor.startMonitoring(session); } } private void doMonitor(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { final StatusExposingByteCountingServletResponse responseWrapper; HttpServletResponseBufferWrapper httpServletResponseBufferWrapper = null; if (isInjectContentToHtml(request)) { httpServletResponseBufferWrapper = new HttpServletResponseBufferWrapper(response); responseWrapper = new StatusExposingByteCountingServletResponse(httpServletResponseBufferWrapper); } else { responseWrapper = new StatusExposingByteCountingServletResponse(response); } try { final SpanContextInformation spanContext = monitorRequest(filterChain, request, responseWrapper); if (isInjectContentToHtml(request)) { injectHtml(response, request, httpServletResponseBufferWrapper, spanContext); } } catch (Exception e) { handleException(e); } } private boolean isInjectContentToHtml(HttpServletRequest httpServletRequest) { if (logger.isDebugEnabled()) { logger.debug("atLeastServletApi3={} isHtmlRequested={} isAtLeastOneHtmlInjectorActive={}", atLeastServletApi3, isHtmlRequested(httpServletRequest), isAtLeastOneHtmlInjectorActive(httpServletRequest)); } return atLeastServletApi3 && isHtmlRequested(httpServletRequest) && isAtLeastOneHtmlInjectorActive(httpServletRequest); } private boolean isAtLeastOneHtmlInjectorActive(HttpServletRequest httpServletRequest) { for (HtmlInjector htmlInjector : htmlInjectors) { if (htmlInjector.isActive(new HtmlInjector.IsActiveArguments(httpServletRequest))) { return true; } } return false; } private boolean isHtmlRequested(HttpServletRequest httpServletRequest) { final String accept = httpServletRequest.getHeader("accept"); return accept != null && accept.contains("text/html"); } private boolean isInternalRequest(HttpServletRequest request) { return request.getRequestURI().startsWith(request.getContextPath() + "/stagemonitor"); } protected SpanContextInformation monitorRequest(FilterChain filterChain, HttpServletRequest httpServletRequest, StatusExposingByteCountingServletResponse responseWrapper) throws Exception { final MonitoredHttpRequest monitoredRequest = monitoredHttpRequestFactory.createMonitoredHttpRequest(httpServletRequest, responseWrapper, filterChain, configuration); return requestMonitor.monitor(monitoredRequest); } protected void injectHtml(HttpServletResponse response, HttpServletRequest httpServletRequest, HttpServletResponseBufferWrapper httpServletResponseBufferWrapper, SpanContextInformation spanContext) throws IOException { logger.debug("injectHtml: contentType={}", httpServletResponseBufferWrapper.getContentType()); if (httpServletResponseBufferWrapper.getContentType() != null && httpServletResponseBufferWrapper.getContentType().contains("text/html") && httpServletRequest.getAttribute("stagemonitorInjected") == null) { if (httpServletResponseBufferWrapper.isUsingWriter()) { injectHtmlToWriter(response, httpServletRequest, httpServletResponseBufferWrapper, spanContext); } else { injectHtmlToOutputStream(response, httpServletRequest, httpServletResponseBufferWrapper, spanContext); } } else { passthrough(response, httpServletResponseBufferWrapper); } } private void injectHtmlToOutputStream(HttpServletResponse response, HttpServletRequest httpServletRequest, HttpServletResponseBufferWrapper httpServletResponseBufferWrapper, SpanContextInformation spanContext) throws IOException { logger.debug("injectHtmlToOutputStream - encoding={}", response.getCharacterEncoding()); String content = new String(httpServletResponseBufferWrapper.getOutputStream().getOutput().toByteArray(), response.getCharacterEncoding()); if (content.contains("</body>")) { httpServletRequest.setAttribute("stagemonitorInjected", true); content = getContetToInject(httpServletRequest, spanContext, content); final byte[] bytes = content.getBytes(response.getCharacterEncoding()); response.setContentLength(bytes.length); response.getOutputStream().write(bytes); } else { // this is no html passthrough(response, httpServletResponseBufferWrapper); } } private void injectHtmlToWriter(ServletResponse response, HttpServletRequest httpServletRequest, HttpServletResponseBufferWrapper httpServletResponseBufferWrapper, SpanContextInformation spanContext) throws IOException { logger.debug("injectHtmlToWriter - encoding={}", response.getCharacterEncoding()); httpServletRequest.setAttribute("stagemonitorInjected", true); String content = httpServletResponseBufferWrapper.getWriter().getOutput().toString(); content = getContetToInject(httpServletRequest, spanContext, content); response.getWriter().write(content); } private String getContetToInject(HttpServletRequest httpServletRequest, SpanContextInformation spanContext, String content) { for (HtmlInjector htmlInjector : htmlInjectors) { if (htmlInjector.isActive(new HtmlInjector.IsActiveArguments(httpServletRequest))) { final HtmlInjector.InjectArguments injectArguments = new HtmlInjector.InjectArguments(spanContext); try { htmlInjector.injectHtml(injectArguments); } catch (Exception e) { logger.warn(e.getMessage() + "(this exception was suppressed)", e); } content = injectBeforeClosingBody(content, injectArguments); } } return content; } private void passthrough(ServletResponse originalResponse, HttpServletResponseBufferWrapper responseWrapper) throws IOException { if (originalResponse.isCommitted()) { return; } if (responseWrapper.isUsingWriter()) { originalResponse.getWriter().write(responseWrapper.getWriter().getOutput().toString()); } else { ByteArrayOutputStream output = responseWrapper.getOutputStream().getOutput(); output.writeTo(originalResponse.getOutputStream()); } } private String injectBeforeClosingBody(String unmodifiedContent, HtmlInjector.InjectArguments injectArguments) { final int lastClosingBodyIndex = unmodifiedContent.lastIndexOf("</body>"); final String modifiedContent; if (injectArguments.getContentToInjectBeforeClosingBody() != null && lastClosingBodyIndex > -1) { final StringBuilder modifiedContentStringBuilder = new StringBuilder(unmodifiedContent.length() + injectArguments.getContentToInjectBeforeClosingBody().length()); modifiedContentStringBuilder.append(unmodifiedContent.substring(0, lastClosingBodyIndex)); modifiedContentStringBuilder.append(injectArguments.getContentToInjectBeforeClosingBody()); modifiedContentStringBuilder.append(unmodifiedContent.substring(lastClosingBodyIndex)); modifiedContent = modifiedContentStringBuilder.toString(); } else { // no body close tag found - pass through without injection modifiedContent = unmodifiedContent; } return modifiedContent; } protected void handleException(Exception e) throws IOException, ServletException { if (e instanceof IOException) { throw (IOException) e; } if (e instanceof ServletException) { throw (ServletException) e; } if (e instanceof RuntimeException) { throw (RuntimeException) e; } else { throw new RuntimeException(e); } } @Override public void destroy() { Stagemonitor.shutDown(); } }