001package io.prometheus.client.servlet.common.filter;
002
003import io.prometheus.client.*;
004import io.prometheus.client.servlet.common.adapter.*;
005
006/**
007 * Filter implements the common functionality provided by the two MetricsFilter implementations:
008 * <ul>
009 * <li>javax version: {@code io.prometheus.client.filter.MetricsFilter} provided by {@code simpleclient_servlet}
010 * <li>jakarta version: {@code io.prometheus.client.servlet.jakarta.filter.MetricsFilter} provided by {@code simpleclient_servlet_jakarta}
011 * </ul>
012 * @author Andrew Stuart &lt;andrew.stuart2@gmail.com&gt;
013 */
014public class Filter {
015    static final String PATH_COMPONENT_PARAM = "path-components";
016    static final String HELP_PARAM = "help";
017    static final String METRIC_NAME_PARAM = "metric-name";
018    static final String BUCKET_CONFIG_PARAM = "buckets";
019    static final String STRIP_CONTEXT_PATH_PARAM = "strip-context-path";
020
021    private Histogram histogram = null;
022    private Counter statusCounter = null;
023
024    // Package-level for testing purposes.
025    int pathComponents = 1;
026    private String metricName = null;
027    boolean stripContextPath = false;
028    private String help = "The time taken fulfilling servlet requests";
029    private double[] buckets = null;
030
031    public Filter() {}
032
033    /**
034     * If you want to configure the filter programmatically instead of via {@code web.xml}, you can
035     * pass all configuration parameters to this constructor.
036     */
037    public Filter(
038            String metricName,
039            String help,
040            Integer pathComponents,
041            double[] buckets,
042            boolean stripContextPath) {
043        this.metricName = metricName;
044        this.buckets = buckets;
045        if (help != null) {
046            this.help = help;
047        }
048        if (pathComponents != null) {
049            this.pathComponents = pathComponents;
050        }
051        this.stripContextPath = stripContextPath;
052    }
053
054    private boolean isEmpty(String s) {
055        return s == null || s.length() == 0;
056    }
057
058    private String getComponents(String str) {
059        if (str == null || pathComponents < 1) {
060            return str;
061        }
062        int count = 0;
063        int i =  -1;
064        do {
065            i = str.indexOf("/", i + 1);
066            if (i < 0) {
067                // Path is longer than specified pathComponents.
068                return str;
069            }
070            count++;
071        } while (count <= pathComponents);
072
073        return str.substring(0, i);
074    }
075
076    /**
077     * Common implementation of {@code javax.servlet.Filter.init()} and {@code jakarta.servlet.Filter.init()}.
078     */
079    public void init(FilterConfigAdapter filterConfig) throws FilterConfigurationException {
080        Histogram.Builder builder = Histogram.build()
081                .labelNames("path", "method");
082
083        if (filterConfig == null && isEmpty(metricName)) {
084            throw new FilterConfigurationException("No configuration object provided, and no metricName passed via constructor");
085        }
086
087        if (filterConfig != null) {
088            if (isEmpty(metricName)) {
089                metricName = filterConfig.getInitParameter(METRIC_NAME_PARAM);
090                if (isEmpty(metricName)) {
091                    throw new FilterConfigurationException("Init parameter \"" + METRIC_NAME_PARAM + "\" is required; please supply a value");
092                }
093            }
094
095            if (!isEmpty(filterConfig.getInitParameter(HELP_PARAM))) {
096                help = filterConfig.getInitParameter(HELP_PARAM);
097            }
098
099            // Allow users to override the default bucket configuration
100            if (!isEmpty(filterConfig.getInitParameter(BUCKET_CONFIG_PARAM))) {
101                String[] bucketParams = filterConfig.getInitParameter(BUCKET_CONFIG_PARAM).split(",");
102                buckets = new double[bucketParams.length];
103
104                for (int i = 0; i < bucketParams.length; i++) {
105                    buckets[i] = Double.parseDouble(bucketParams[i]);
106                }
107            }
108
109            // Allow overriding of the path "depth" to track
110            if (!isEmpty(filterConfig.getInitParameter(PATH_COMPONENT_PARAM))) {
111                pathComponents = Integer.parseInt(filterConfig.getInitParameter(PATH_COMPONENT_PARAM));
112            }
113
114            if (!isEmpty(filterConfig.getInitParameter(STRIP_CONTEXT_PATH_PARAM))) {
115                stripContextPath = Boolean.parseBoolean(filterConfig.getInitParameter(STRIP_CONTEXT_PATH_PARAM));
116            }
117        }
118
119        if (buckets != null) {
120            builder = builder.buckets(buckets);
121        }
122
123        histogram = builder
124                .help(help)
125                .name(metricName)
126                .register();
127
128        statusCounter = Counter.build(metricName + "_status_total", "HTTP status codes of " + help)
129                .labelNames("path", "method", "status")
130                .register();
131    }
132
133    /**
134     * To be called at the beginning of {@code javax.servlet.Filter.doFilter()} or
135     * {@code jakarta.servlet.Filter.doFilter()}.
136     */
137    public MetricData startTimer(HttpServletRequestAdapter request) {
138        String path = request.getRequestURI();
139        if (stripContextPath) {
140            path = path.substring(request.getContextPath().length());
141        }
142        String components = getComponents(path);
143        String method = request.getMethod();
144        Histogram.Timer timer = histogram.labels(components, method).startTimer();
145        return new MetricData(components, method, timer);
146    }
147
148    /**
149     * To be called at the end of {@code javax.servlet.Filter.doFilter()} or
150     * {@code jakarta.servlet.Filter.doFilter()}.
151     */
152    public void observeDuration(MetricData data, HttpServletResponseAdapter resp) {
153        String status = Integer.toString(resp.getStatus());
154        data.timer.observeDuration();
155        statusCounter.labels(data.components, data.method, status).inc();
156    }
157
158    public static class MetricData {
159
160        final String components;
161        final String method;
162        final Histogram.Timer timer;
163
164        private MetricData(String components, String method, Histogram.Timer timer) {
165            this.components = components;
166            this.method = method;
167            this.timer = timer;
168        }
169    }
170}