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    ) throws ServletException {
076        this.metricName = metricName;
077        this.buckets = buckets;
078        if (help != null) {
079            this.help = help;
080        }
081        if (pathComponents != null) {
082            this.pathComponents = pathComponents;
083        }
084    }
085
086    private boolean isEmpty(String s) {
087        return s == null || s.length() == 0;
088    }
089
090    private String getComponents(String str) {
091        if (str == null || pathComponents < 1) {
092            return str;
093        }
094        int count = 0;
095        int i =  -1;
096        do {
097            i = str.indexOf("/", i + 1);
098            if (i < 0) {
099                // Path is longer than specified pathComponents.
100                return str;
101            }
102            count++;
103        } while (count <= pathComponents);
104
105        return str.substring(0, i);
106    }
107
108    @Override
109    public void init(FilterConfig filterConfig) throws ServletException {
110        Histogram.Builder builder = Histogram.build()
111                .labelNames("path", "method");
112
113        if (filterConfig == null && isEmpty(metricName)) {
114            throw new ServletException("No configuration object provided, and no metricName passed via constructor");
115        }
116
117        if (filterConfig != null) {
118            if (isEmpty(metricName)) {
119                metricName = filterConfig.getInitParameter(METRIC_NAME_PARAM);
120                if (isEmpty(metricName)) {
121                    throw new ServletException("Init parameter \"" + METRIC_NAME_PARAM + "\" is required; please supply a value");
122                }
123            }
124
125            if (!isEmpty(filterConfig.getInitParameter(HELP_PARAM))) {
126                help = filterConfig.getInitParameter(HELP_PARAM);
127            }
128
129            // Allow overriding of the path "depth" to track
130            if (!isEmpty(filterConfig.getInitParameter(PATH_COMPONENT_PARAM))) {
131                pathComponents = Integer.valueOf(filterConfig.getInitParameter(PATH_COMPONENT_PARAM));
132            }
133
134            // Allow users to override the default bucket configuration
135            if (!isEmpty(filterConfig.getInitParameter(BUCKET_CONFIG_PARAM))) {
136                String[] bucketParams = filterConfig.getInitParameter(BUCKET_CONFIG_PARAM).split(",");
137                buckets = new double[bucketParams.length];
138
139                for (int i = 0; i < bucketParams.length; i++) {
140                    buckets[i] = Double.parseDouble(bucketParams[i]);
141                }
142            }
143        }
144
145        if (buckets != null) {
146            builder = builder.buckets(buckets);
147        }
148
149        histogram = builder
150                .help(help)
151                .name(metricName)
152                .register();
153    }
154
155    @Override
156    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
157        if (!(servletRequest instanceof HttpServletRequest)) {
158            filterChain.doFilter(servletRequest, servletResponse);
159            return;
160        }
161
162        HttpServletRequest request = (HttpServletRequest) servletRequest;
163
164        String path = request.getRequestURI();
165
166        Histogram.Timer timer = histogram
167            .labels(getComponents(path), request.getMethod())
168            .startTimer();
169
170        try {
171            filterChain.doFilter(servletRequest, servletResponse);
172        } finally {
173            timer.observeDuration();
174        }
175    }
176
177    @Override
178    public void destroy() {
179    }
180}