Skip to content

Commit

Permalink
Add http metrics http_request_duration_seconds and http_requests_total
Browse files Browse the repository at this point in the history
.) 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 ...)
  • Loading branch information
goettl79 committed May 5, 2017
1 parent 5b0acb2 commit 354920c
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 2 deletions.
13 changes: 11 additions & 2 deletions src/main/java/com/gitblit/guice/WebModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();

Expand All @@ -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);
Expand Down
134 changes: 134 additions & 0 deletions src/main/java/com/gitblit/servlet/MetricsFilter.java
Original file line number Diff line number Diff line change
@@ -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() {
}

}

50 changes: 50 additions & 0 deletions src/test/java/com/gitblit/tests/MetricsFilterTest.java
Original file line number Diff line number Diff line change
@@ -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);
}

}

0 comments on commit 354920c

Please sign in to comment.