001package io.prometheus.client.filter;
002
003import io.prometheus.client.Histogram;
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 java.io.IOException;
013
014/**
015 * The MetricsFilter class exists to provide a high-level filter that enables tunable collection of metrics for Servlet
016 * performance.
017 *
018 * The Histogram name itself is required, and configured with a {@code metric-name} init parameter.
019 *
020 * The help parameter, configured with the {@code help} init parameter, is not required but strongly recommended.
021 *
022 * By default, this filter will provide metrics that distinguish only 1 level deep for the request path
023 * (including servlet context path), but can be configured with the {@code path-components} init parameter. Any number
024 * provided that is less than 1 will provide the full path granularity (warning, this may affect performance).
025 *
026 * The Histogram buckets can be configured with a {@code buckets} init parameter whose value is a comma-separated list
027 * of valid {@code double} values.
028 *
029 * {@code
030 * <filter>
031 *   <filter-name>prometheusFilter</filter-name>
032 *   <filter-class>net.cccnext.ssp.portal.spring.filter.PrometheusMetricsFilter</filter-class>
033 *   <init-param>
034 *      <param-name>metric-name</param-name>
035 *      <param-value>webapp_metrics_filter</param-value>
036 *   </init-param>
037 *    <init-param>
038 *      <param-name>help</param-name>
039 *      <param-value>The time taken fulfilling servlet requests</param-value>
040 *   </init-param>
041 *   <init-param>
042 *      <param-name>buckets</param-name>
043 *      <param-value>0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1,2.5,5,7.5,10</param-value>
044 *   </init-param>
045 *   <init-param>
046 *      <param-name>path-components</param-name>
047 *      <param-value>0</param-value>
048 *   </init-param>
049 * </filter>
050 * }
051 *
052 * @author Andrew Stuart &lt;andrew.stuart2@gmail.com&gt;
053 */
054public class MetricsFilter implements Filter {
055    static final String PATH_COMPONENT_PARAM = "path-components";
056    static final String HELP_PARAM = "help";
057    static final String METRIC_NAME_PARAM = "metric-name";
058    static final String BUCKET_CONFIG_PARAM = "buckets";
059
060    private Histogram histogram = null;
061
062    // Package-level for testing purposes.
063    int pathComponents = 1;
064    private String metricName = null;
065    private String help = "The time taken fulfilling servlet requests";
066    private double[] buckets = null;
067
068    public MetricsFilter() {}
069
070    public MetricsFilter(
071            String metricName,
072            String help,
073            Integer pathComponents,
074            double[] buckets) {
075        this.metricName = metricName;
076        this.buckets = buckets;
077        if (help != null) {
078            this.help = help;
079        }
080        if (pathComponents != null) {
081            this.pathComponents = pathComponents;
082        }
083    }
084
085    private boolean isEmpty(String s) {
086        return s == null || s.length() == 0;
087    }
088
089    private String getComponents(String str) {
090        if (str == null || pathComponents < 1) {
091            return str;
092        }
093        int count = 0;
094        int i =  -1;
095        do {
096            i = str.indexOf("/", i + 1);
097            if (i < 0) {
098                // Path is longer than specified pathComponents.
099                return str;
100            }
101            count++;
102        } while (count <= pathComponents);
103
104        return str.substring(0, i);
105    }
106
107    @Override
108    public void init(FilterConfig filterConfig) throws ServletException {
109        Histogram.Builder builder = Histogram.build()
110                .labelNames("path", "method");
111
112        if (filterConfig == null && isEmpty(metricName)) {
113            throw new ServletException("No configuration object provided, and no metricName passed via constructor");
114        }
115
116        if (filterConfig != null) {
117            if (isEmpty(metricName)) {
118                metricName = filterConfig.getInitParameter(METRIC_NAME_PARAM);
119                if (isEmpty(metricName)) {
120                    throw new ServletException("Init parameter \"" + METRIC_NAME_PARAM + "\" is required; please supply a value");
121                }
122            }
123
124            if (!isEmpty(filterConfig.getInitParameter(HELP_PARAM))) {
125                help = filterConfig.getInitParameter(HELP_PARAM);
126            }
127
128            // Allow overriding of the path "depth" to track
129            if (!isEmpty(filterConfig.getInitParameter(PATH_COMPONENT_PARAM))) {
130                pathComponents = Integer.valueOf(filterConfig.getInitParameter(PATH_COMPONENT_PARAM));
131            }
132
133            // Allow users to override the default bucket configuration
134            if (!isEmpty(filterConfig.getInitParameter(BUCKET_CONFIG_PARAM))) {
135                String[] bucketParams = filterConfig.getInitParameter(BUCKET_CONFIG_PARAM).split(",");
136                buckets = new double[bucketParams.length];
137
138                for (int i = 0; i < bucketParams.length; i++) {
139                    buckets[i] = Double.parseDouble(bucketParams[i]);
140                }
141            }
142        }
143
144        if (buckets != null) {
145            builder = builder.buckets(buckets);
146        }
147
148        histogram = builder
149                .help(help)
150                .name(metricName)
151                .register();
152    }
153
154    @Override
155    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
156        if (!(servletRequest instanceof HttpServletRequest)) {
157            filterChain.doFilter(servletRequest, servletResponse);
158            return;
159        }
160
161        HttpServletRequest request = (HttpServletRequest) servletRequest;
162
163        String path = request.getRequestURI();
164
165        Histogram.Timer timer = histogram
166            .labels(getComponents(path), request.getMethod())
167            .startTimer();
168
169        try {
170            filterChain.doFilter(servletRequest, servletResponse);
171        } finally {
172            timer.observeDuration();
173        }
174    }
175
176    @Override
177    public void destroy() {
178    }
179}