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