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        synchronized (this){
042            if (servletLatency == null) {
043                Histogram.Builder servletLatencyBuilder = Histogram.build()
044                        .name("servlet_request_seconds")
045                        .help("The time taken fulfilling servlet requests")
046                        .labelNames("context", "method");
047
048                if ((filterConfig.getInitParameter(BUCKET_CONFIG_PARAM) != null) && (!filterConfig.getInitParameter(BUCKET_CONFIG_PARAM).isEmpty())) {
049                    String[] bucketParams = filterConfig.getInitParameter(BUCKET_CONFIG_PARAM).split(",");
050                    double[] buckets = new double[bucketParams.length];
051                    for (int i = 0; i < bucketParams.length; i++) {
052                        buckets[i] = Double.parseDouble(bucketParams[i].trim());
053                    }
054                    servletLatencyBuilder.buckets(buckets);
055                } else {
056                    servletLatencyBuilder.buckets(.01, .05, .1, .25, .5, 1, 2.5, 5, 10, 30);
057                }
058
059                servletLatency = servletLatencyBuilder.register();
060
061                Gauge.Builder servletConcurrentRequestBuilder = Gauge.build()
062                        .name("servlet_request_concurrent_total")
063                        .help("Number of concurrent requests for given context.")
064                        .labelNames("context");
065
066                servletConcurrentRequest = servletConcurrentRequestBuilder.register();
067
068                Gauge.Builder servletStatusCodesBuilder = Gauge.build()
069                        .name("servlet_response_status_total")
070                        .help("Number of requests for given context and status code.")
071                        .labelNames("context", "status");
072
073                servletStatusCodes = servletStatusCodesBuilder.register();
074
075            }
076        }
077    }
078
079    @Override
080    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
081        if (!(servletRequest instanceof HttpServletRequest)) {
082            filterChain.doFilter(servletRequest, servletResponse);
083            return;
084        }
085
086        HttpServletRequest request = (HttpServletRequest) servletRequest;
087
088        if (!request.isAsyncStarted()) {
089            String context = getContext(request);
090
091            servletConcurrentRequest.labels(context).inc();
092
093            Histogram.Timer timer = servletLatency
094                    .labels(context, request.getMethod())
095                    .startTimer();
096
097            try {
098                filterChain.doFilter(servletRequest, servletResponse);
099            } finally {
100                timer.observeDuration();
101                servletConcurrentRequest.labels(context).dec();
102                servletStatusCodes.labels(context, Integer.toString(getStatus((HttpServletResponse) servletResponse))).inc();
103            }
104        } else {
105            filterChain.doFilter(servletRequest, servletResponse);
106        }
107    }
108
109    private int getStatus(HttpServletResponse response) {
110        try {
111            return response.getStatus();
112        } catch (Exception ex) {
113            return UNDEFINED_HTTP_STATUS;
114        }
115    }
116
117    private String getContext(HttpServletRequest request) {
118        if (request.getContextPath() != null && !request.getContextPath().isEmpty()) {
119            return request.getContextPath();
120        } else {
121            return "/";
122        }
123    }
124
125    @Override
126    public void destroy() {
127        // NOOP
128    }
129}