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}