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 ) 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}