001package nl.nlighten.prometheus.tomcat; 002 003import io.prometheus.client.*; 004import javax.servlet.Filter; 005import javax.servlet.FilterChain; 006import javax.servlet.FilterConfig; 007import javax.servlet.ServletException; 008import javax.servlet.ServletRequest; 009import javax.servlet.ServletResponse; 010import javax.servlet.annotation.WebFilter; 011import javax.servlet.http.HttpServletRequest; 012import javax.servlet.http.HttpServletResponse; 013import java.io.IOException; 014 015/** 016 * A servlet filter that can be configured in Tomcat's global web.xml and that provides the following metrics: 017 * 018 * - A Histogram with response time distribution per context 019 * - A Gauge with the number of concurrent request per context 020 * - A Gauge with a the number of responses per context and status code 021 * 022 * <p> 023 * If you are running Tomcat in the conventional non-embedded way you should add the client_tomcat jar and all its 024 * dependencies (see POM.XML) to the $CATALINA_BASE/lib directory or another directory on the common.loader path. 025 * Next, add this filter to the $CATALINA_BASE/lib/web.xml, e.g.: 026 * 027 * <pre> 028 * {@code 029 * <filter> 030 * <filter-name>ServletMetricsFilter</filter-name> 031 * <filter-class>nl.nlighten.prometheus.TomcatServletMetricsFilter</filter-class> 032 * <async-supported>true</async-supported> 033 * <init-param> 034 * <param-name>buckets</param-name> 035 * <param-value>.01, .05, .1, .25, .5, 1, 2.5, 5, 10, 30</param-value> 036 * </init-param> 037 * </filter> 038 * } 039 * </pre> 040 * 041 * If you running Tomcat embedded, please check AbstractTomcatMetricsTest for example configuration. 042 * 043 * Example metrics being exported: 044 * <pre> 045 * servlet_request_seconds_bucket{"/foo", "GET", "0.1",} 1.0 046 * .... 047 * servlet_request_seconds_bucket{"/foo", "GET", "+Inf",} 1.0 048 * servlet_request_concurrent_total{"/foo",} 1.0 049 * servlet_response_status_total{"/foo", "200",} 1.0 050 * </pre> 051 */ 052public class TomcatServletMetricsFilter implements Filter { 053 private static final String BUCKET_CONFIG_PARAM = "buckets"; 054 private static Histogram servletLatency; 055 private static Gauge servletConcurrentRequest; 056 private static Gauge servletStatusCodes; 057 058 private static int UNDEFINED_HTTP_STATUS = 999; 059 060 @Override 061 public void init(FilterConfig filterConfig) throws ServletException { 062 if (servletLatency == null) { 063 Histogram.Builder servletLatencyBuilder = Histogram.build() 064 .name("servlet_request_seconds") 065 .help("The time taken fulfilling servlet requests") 066 .labelNames("context", "method"); 067 068 if ((filterConfig.getInitParameter(BUCKET_CONFIG_PARAM) != null) && (!filterConfig.getInitParameter(BUCKET_CONFIG_PARAM).isEmpty())) { 069 String[] bucketParams = filterConfig.getInitParameter(BUCKET_CONFIG_PARAM).split(","); 070 double[] buckets = new double[bucketParams.length]; 071 for (int i = 0; i < bucketParams.length; i++) { 072 buckets[i] = Double.parseDouble(bucketParams[i]); 073 } 074 servletLatencyBuilder.buckets(buckets); 075 } else { 076 servletLatencyBuilder.buckets(.01, .05, .1, .25, .5, 1, 2.5, 5, 10, 30); 077 } 078 079 servletLatency = servletLatencyBuilder.register(); 080 081 Gauge.Builder servletConcurrentRequestBuilder = Gauge.build() 082 .name("servlet_request_concurrent_total") 083 .help("Number of concurrent requests for given context.") 084 .labelNames("context"); 085 086 servletConcurrentRequest = servletConcurrentRequestBuilder.register(); 087 088 Gauge.Builder servletStatusCodesBuilder = Gauge.build() 089 .name("servlet_response_status_total") 090 .help("Number of requests for given context and status code.") 091 .labelNames("context", "status"); 092 093 servletStatusCodes = servletStatusCodesBuilder.register(); 094 095 } 096 } 097 098 @Override 099 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 100 if (!(servletRequest instanceof HttpServletRequest)) { 101 filterChain.doFilter(servletRequest, servletResponse); 102 return; 103 } 104 105 HttpServletRequest request = (HttpServletRequest) servletRequest; 106 107 if (!request.isAsyncStarted()) { 108 String context = getContext(request); 109 110 servletConcurrentRequest.labels(context).inc(); 111 112 Histogram.Timer timer = servletLatency 113 .labels(context, request.getMethod()) 114 .startTimer(); 115 116 try { 117 filterChain.doFilter(servletRequest, servletResponse); 118 } finally { 119 timer.observeDuration(); 120 servletConcurrentRequest.labels(context).dec(); 121 servletStatusCodes.labels(context, Integer.toString(getStatus((HttpServletResponse) servletResponse))).inc(); 122 } 123 } else { 124 filterChain.doFilter(servletRequest, servletResponse); 125 } 126 } 127 128 private int getStatus(HttpServletResponse response) { 129 try { 130 return response.getStatus(); 131 } catch (Exception ex) { 132 return UNDEFINED_HTTP_STATUS; 133 } 134 } 135 136 private String getContext(HttpServletRequest request) { 137 if (request.getContextPath() != null && !request.getContextPath().isEmpty()) { 138 return request.getContextPath(); 139 } else { 140 return "/"; 141 } 142 } 143 144 @Override 145 public void destroy() { 146 // NOOP 147 } 148}