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}