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 <andrew.stuart2@gmail.com> 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}