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