From 354920c46964cb3ccdc3182be2dca96f2c6cf968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georg=20=C3=96ttl?= Date: Fri, 5 May 2017 11:45:22 +0200 Subject: [PATCH] Add http metrics http_request_duration_seconds and http_requests_total .) http_request_duration_seconds (histogram) -> Labels (path,method) .) http_requests_total (counter) -> Labels (path,method,status) Where path is the REST path of the ressource, method is the HTTP method (GET,PUT..) and status is the HTTP response code (2xx, 3xx ...) --- .../java/com/gitblit/guice/WebModule.java | 13 +- .../com/gitblit/servlet/MetricsFilter.java | 134 ++++++++++++++++++ .../com/gitblit/tests/MetricsFilterTest.java | 50 +++++++ 3 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/gitblit/servlet/MetricsFilter.java create mode 100644 src/test/java/com/gitblit/tests/MetricsFilterTest.java diff --git a/src/main/java/com/gitblit/guice/WebModule.java b/src/main/java/com/gitblit/guice/WebModule.java index 7d5f2d452..af2545806 100644 --- a/src/main/java/com/gitblit/guice/WebModule.java +++ b/src/main/java/com/gitblit/guice/WebModule.java @@ -30,6 +30,7 @@ import com.gitblit.servlet.GitFilter; import com.gitblit.servlet.GitServlet; import com.gitblit.servlet.LogoServlet; +import com.gitblit.servlet.MetricsFilter; import com.gitblit.servlet.PagesFilter; import com.gitblit.servlet.PagesServlet; import com.gitblit.servlet.ProxyFilter; @@ -43,7 +44,9 @@ import com.gitblit.servlet.SyndicationFilter; import com.gitblit.servlet.SyndicationServlet; import com.gitblit.wicket.GitblitWicketFilter; + import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; import com.google.inject.Scopes; import com.google.inject.servlet.ServletModule; import io.prometheus.client.exporter.MetricsServlet; @@ -84,6 +87,7 @@ protected void configureServlets() { // Prometheus bind(MetricsServlet.class).in(Scopes.SINGLETON); + bind(MetricsFilter.class).in(Scopes.SINGLETON); serve("/prometheus").with(MetricsServlet.class); DefaultExports.initialize(); @@ -99,8 +103,13 @@ protected void configureServlets() { serve(fuzzy("/com/")).with(AccessDeniedServlet.class); // global filters - filter(ALL).through(ProxyFilter.class); - filter(ALL).through(EnforceAuthenticationFilter.class); + filter(ALL).through(MetricsFilter.class, + ImmutableMap.of( + MetricsFilter.PARAM_DURATION_HIST_BUCKET_CONFIG, "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", + MetricsFilter.PARAM_PATH_MAX_DEPTH, "16" + )); + filter(ALL).through(ProxyFilter.class); + filter(ALL).through(EnforceAuthenticationFilter.class); // security filters filter(fuzzy(Constants.R_PATH), fuzzy(Constants.GIT_PATH)).through(GitFilter.class); diff --git a/src/main/java/com/gitblit/servlet/MetricsFilter.java b/src/main/java/com/gitblit/servlet/MetricsFilter.java new file mode 100644 index 000000000..1595bf9ed --- /dev/null +++ b/src/main/java/com/gitblit/servlet/MetricsFilter.java @@ -0,0 +1,134 @@ +package com.gitblit.servlet; + +import io.prometheus.client.Counter; +import io.prometheus.client.Histogram; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + */ +public class MetricsFilter implements Filter { + public static final String PARAM_PATH_MAX_DEPTH = "max-path-depth"; + public static final String PARAM_DURATION_HIST_BUCKET_CONFIG = "request-duration-histogram-buckets"; + + private Histogram httpRequestDuration = null; + private Counter requests = null; + + // Package-level for testing purposes. + int pathDepth = 1; + private double[] buckets = null; + + public MetricsFilter() { + } + + public MetricsFilter( + Integer maxPathDepth, + double[] buckets + ) throws ServletException { + this.buckets = buckets; + if (maxPathDepth != null) { + this.pathDepth = maxPathDepth; + } + this.init(null); + } + + private boolean isEmpty(String s) { + return s == null || s.length() == 0; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + Histogram.Builder httpRequestDurationBuilder = Histogram.build() + .name("http_request_duration_seconds") + .labelNames("path", "method") + .help("The time taken fulfilling servlet requests"); + + Counter.Builder requestsBuilder = Counter.build() + .name("http_requests_total") + .help("Total requests.") + .labelNames("path", "method", "status"); + + if (filterConfig == null && isEmpty("http_request_duration")) { + throw new ServletException("No configuration object provided, and no metricName passed via constructor"); + } + + if (filterConfig != null) { + + // Allow overriding of the path "depth" to track + if (!isEmpty(filterConfig.getInitParameter(PARAM_PATH_MAX_DEPTH))) { + pathDepth = Integer.valueOf(filterConfig.getInitParameter(PARAM_PATH_MAX_DEPTH)); + } + + // Allow users to override the default bucket configuration + if (!isEmpty(filterConfig.getInitParameter(PARAM_DURATION_HIST_BUCKET_CONFIG))) { + String[] bucketParams = filterConfig.getInitParameter(PARAM_DURATION_HIST_BUCKET_CONFIG).split(","); + buckets = new double[bucketParams.length]; + + for (int i = 0; i < bucketParams.length; i++) { + buckets[i] = Double.parseDouble(bucketParams[i]); + } + } + } + + requests = requestsBuilder.register(); + + if (buckets != null) { + httpRequestDurationBuilder = httpRequestDurationBuilder.buckets(buckets); + } + + httpRequestDuration = httpRequestDurationBuilder.register(); + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + if (!(servletRequest instanceof HttpServletRequest)) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + String path = request.getRequestURI(); + String normalizedPath = extractPathFrom(path, pathDepth); + + Histogram.Timer timer = httpRequestDuration + .labels(normalizedPath, request.getMethod()) + .startTimer(); + try { + filterChain.doFilter(servletRequest, servletResponse); + requests.labels(normalizedPath, request.getMethod().toUpperCase(), String.valueOf(response.getStatus())).inc(); + } finally { + timer.observeDuration(); + } + } + + public String extractPathFrom(String requestUri, int maxPathDepth) { + if (maxPathDepth < 0 || requestUri == null) { + throw new IllegalArgumentException("Path depth has to >= 0"); + } + + int count = 0; + int pathPosition = -1; + do { + int lastPathPosition = pathPosition; + pathPosition = requestUri.indexOf("/", pathPosition + 1); + if (count > maxPathDepth || pathPosition < 0) { + return requestUri.substring(0, lastPathPosition + 1); + } + count++; + } while (count <= maxPathDepth); + + return requestUri.substring(0, pathPosition + 1); + } + + @Override + public void destroy() { + } + +} + diff --git a/src/test/java/com/gitblit/tests/MetricsFilterTest.java b/src/test/java/com/gitblit/tests/MetricsFilterTest.java new file mode 100644 index 000000000..5b7623294 --- /dev/null +++ b/src/test/java/com/gitblit/tests/MetricsFilterTest.java @@ -0,0 +1,50 @@ +package com.gitblit.tests; + +import com.gitblit.servlet.MetricsFilter; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + + +public class MetricsFilterTest { + + @Test + public void alwaysExtractRootPathForZeroPathLength() { + MetricsFilter metricsFilter = new MetricsFilter(); + String path = metricsFilter.extractPathFrom("/index.html", 0); + assertThat(path, equalTo("/")); + } + + @Test + public void useAlwaysRootPathForLongPathLength() { + MetricsFilter metricsFilter = new MetricsFilter(); + String path = metricsFilter.extractPathFrom("/index.html", 1); + assertThat(path, equalTo("/")); + } + + @Test + public void pathDepthOneuseAlwaysRootPathForZeroPathLength() { + MetricsFilter metricsFilter = new MetricsFilter(); + String path = metricsFilter.extractPathFrom("/test/index.html", 1); + assertThat(path, equalTo("/test/")); + } + + @Test + public void cutsPathsLongerThanPathDepth() { + MetricsFilter metricsFilter = new MetricsFilter(); + String path = metricsFilter.extractPathFrom("/test/tralala/index.html", 1); + assertThat(path, equalTo("/test/")); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsExceptionForNegativePathDepth() { + new MetricsFilter().extractPathFrom("/index.html", -1); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsExceptionForNullRequestPath() { + new MetricsFilter().extractPathFrom(null, 1); + } + +} \ No newline at end of file