001package nl.nlighten.prometheus.wildfly;
002
003import io.prometheus.client.*;
004
005import javax.servlet.Filter;
006import javax.servlet.FilterChain;
007import javax.servlet.FilterConfig;
008import javax.servlet.ServletException;
009import javax.servlet.ServletRequest;
010import javax.servlet.ServletResponse;
011import javax.servlet.http.HttpServletRequest;
012import javax.servlet.http.HttpServletResponse;
013import java.io.IOException;
014
015/**
016 * A servlet filter that will be added to deployed servlets by the MetricFilterExtension that provides the following metrics:
017 * <p>
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 * </p>
022 * Example metrics being exported:
023 * <pre>
024 *     servlet_request_seconds_bucket{"/foo", "GET", "0.1",} 1.0
025 *     ....
026 *     servlet_request_seconds_bucket{"/foo", "GET", "+Inf",} 1.0
027 *     servlet_request_concurrent_total{"/foo",} 1.0
028 *     servlet_response_status_total{"/foo", "200",} 1.0
029 *  </pre>
030 */
031public class ServletMetricsFilter implements Filter {
032    public static final String BUCKET_CONFIG_PARAM = "buckets";
033    private static Histogram servletLatency;
034    private static Gauge servletConcurrentRequest;
035    private static Gauge servletStatusCodes;
036
037    private static int UNDEFINED_HTTP_STATUS = 999;
038
039    @Override
040    public void init(FilterConfig filterConfig) throws ServletException {
041        if (servletLatency == null) {
042            Histogram.Builder servletLatencyBuilder = Histogram.build()
043                    .name("servlet_request_seconds")
044                    .help("The time taken fulfilling servlet requests")
045                    .labelNames("context", "method");
046
047            if ((filterConfig.getInitParameter(BUCKET_CONFIG_PARAM) != null) && (!filterConfig.getInitParameter(BUCKET_CONFIG_PARAM).isEmpty())) {
048                String[] bucketParams = filterConfig.getInitParameter(BUCKET_CONFIG_PARAM).split(",");
049                double[] buckets = new double[bucketParams.length];
050                for (int i = 0; i < bucketParams.length; i++) {
051                    buckets[i] = Double.parseDouble(bucketParams[i].trim());
052                }
053                servletLatencyBuilder.buckets(buckets);
054            } else {
055                servletLatencyBuilder.buckets(.01, .05, .1, .25, .5, 1, 2.5, 5, 10, 30);
056            }
057
058            servletLatency = servletLatencyBuilder.register();
059
060            Gauge.Builder servletConcurrentRequestBuilder = Gauge.build()
061                    .name("servlet_request_concurrent_total")
062                    .help("Number of concurrent requests for given context.")
063                    .labelNames("context");
064
065            servletConcurrentRequest = servletConcurrentRequestBuilder.register();
066
067            Gauge.Builder servletStatusCodesBuilder = Gauge.build()
068                    .name("servlet_response_status_total")
069                    .help("Number of requests for given context and status code.")
070                    .labelNames("context", "status");
071
072            servletStatusCodes = servletStatusCodesBuilder.register();
073
074        }
075    }
076
077    @Override
078    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
079        if (!(servletRequest instanceof HttpServletRequest)) {
080            filterChain.doFilter(servletRequest, servletResponse);
081            return;
082        }
083
084        HttpServletRequest request = (HttpServletRequest) servletRequest;
085
086        if (!request.isAsyncStarted()) {
087            String context = getContext(request);
088
089            servletConcurrentRequest.labels(context).inc();
090
091            Histogram.Timer timer = servletLatency
092                    .labels(context, request.getMethod())
093                    .startTimer();
094
095            try {
096                filterChain.doFilter(servletRequest, servletResponse);
097            } finally {
098                timer.observeDuration();
099                servletConcurrentRequest.labels(context).dec();
100                servletStatusCodes.labels(context, Integer.toString(getStatus((HttpServletResponse) servletResponse))).inc();
101            }
102        } else {
103            filterChain.doFilter(servletRequest, servletResponse);
104        }
105    }
106
107    private int getStatus(HttpServletResponse response) {
108        try {
109            return response.getStatus();
110        } catch (Exception ex) {
111            return UNDEFINED_HTTP_STATUS;
112        }
113    }
114
115    private String getContext(HttpServletRequest request) {
116        if (request.getContextPath() != null && !request.getContextPath().isEmpty()) {
117            return request.getContextPath();
118        } else {
119            return "/";
120        }
121    }
122
123    @Override
124    public void destroy() {
125        // NOOP
126    }
127}