Skip to content

Commit 5503195

Browse files
committed
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 ...)
1 parent 5b0acb2 commit 5503195

File tree

3 files changed

+195
-2
lines changed

3 files changed

+195
-2
lines changed

src/main/java/com/gitblit/guice/WebModule.java

+11-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.gitblit.servlet.GitFilter;
3131
import com.gitblit.servlet.GitServlet;
3232
import com.gitblit.servlet.LogoServlet;
33+
import com.gitblit.servlet.MetricsFilter;
3334
import com.gitblit.servlet.PagesFilter;
3435
import com.gitblit.servlet.PagesServlet;
3536
import com.gitblit.servlet.ProxyFilter;
@@ -43,7 +44,9 @@
4344
import com.gitblit.servlet.SyndicationFilter;
4445
import com.gitblit.servlet.SyndicationServlet;
4546
import com.gitblit.wicket.GitblitWicketFilter;
47+
4648
import com.google.common.base.Joiner;
49+
import com.google.common.collect.ImmutableMap;
4750
import com.google.inject.Scopes;
4851
import com.google.inject.servlet.ServletModule;
4952
import io.prometheus.client.exporter.MetricsServlet;
@@ -84,6 +87,7 @@ protected void configureServlets() {
8487

8588
// Prometheus
8689
bind(MetricsServlet.class).in(Scopes.SINGLETON);
90+
bind(MetricsFilter.class).in(Scopes.SINGLETON);
8791
serve("/prometheus").with(MetricsServlet.class);
8892
DefaultExports.initialize();
8993

@@ -99,8 +103,13 @@ protected void configureServlets() {
99103
serve(fuzzy("/com/")).with(AccessDeniedServlet.class);
100104

101105
// global filters
102-
filter(ALL).through(ProxyFilter.class);
103-
filter(ALL).through(EnforceAuthenticationFilter.class);
106+
filter(ALL).through(MetricsFilter.class,
107+
ImmutableMap.of(
108+
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",
109+
MetricsFilter.PARAM_PATH_MAX_DEPTH, "16"
110+
));
111+
filter(ALL).through(ProxyFilter.class);
112+
filter(ALL).through(EnforceAuthenticationFilter.class);
104113

105114
// security filters
106115
filter(fuzzy(Constants.R_PATH), fuzzy(Constants.GIT_PATH)).through(GitFilter.class);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package com.gitblit.servlet;
2+
3+
import io.prometheus.client.Counter;
4+
import io.prometheus.client.Histogram;
5+
6+
import javax.servlet.*;
7+
import javax.servlet.http.HttpServletRequest;
8+
import javax.servlet.http.HttpServletResponse;
9+
import java.io.IOException;
10+
11+
/**
12+
*/
13+
public class MetricsFilter implements Filter {
14+
public static final String PARAM_PATH_MAX_DEPTH = "max-path-depth";
15+
public static final String PARAM_DURATION_HIST_BUCKET_CONFIG = "request-duration-histogram-buckets";
16+
17+
private Histogram httpRequestDuration = null;
18+
private Counter requests = null;
19+
20+
// Package-level for testing purposes.
21+
int pathDepth = 1;
22+
private double[] buckets = null;
23+
24+
public MetricsFilter() {
25+
}
26+
27+
public MetricsFilter(
28+
Integer maxPathDepth,
29+
double[] buckets
30+
) throws ServletException {
31+
this.buckets = buckets;
32+
if (maxPathDepth != null) {
33+
this.pathDepth = maxPathDepth;
34+
}
35+
this.init(null);
36+
}
37+
38+
private boolean isEmpty(String s) {
39+
return s == null || s.length() == 0;
40+
}
41+
42+
@Override
43+
public void init(FilterConfig filterConfig) throws ServletException {
44+
45+
Histogram.Builder httpRequestDurationBuilder = Histogram.build()
46+
.name("http_request_duration_seconds")
47+
.labelNames("path", "method")
48+
.help("The time taken fulfilling servlet requests");
49+
50+
Counter.Builder requestsBuilder = Counter.build()
51+
.name("http_requests_total")
52+
.help("Total requests.")
53+
.labelNames("path", "method", "status");
54+
55+
if (filterConfig == null && isEmpty("http_request_duration")) {
56+
throw new ServletException("No configuration object provided, and no metricName passed via constructor");
57+
}
58+
59+
if (filterConfig != null) {
60+
61+
// Allow overriding of the path "depth" to track
62+
if (!isEmpty(filterConfig.getInitParameter(PARAM_PATH_MAX_DEPTH))) {
63+
pathDepth = Integer.valueOf(filterConfig.getInitParameter(PARAM_PATH_MAX_DEPTH));
64+
}
65+
66+
// Allow users to override the default bucket configuration
67+
if (!isEmpty(filterConfig.getInitParameter(PARAM_DURATION_HIST_BUCKET_CONFIG))) {
68+
String[] bucketParams = filterConfig.getInitParameter(PARAM_DURATION_HIST_BUCKET_CONFIG).split(",");
69+
buckets = new double[bucketParams.length];
70+
71+
for (int i = 0; i < bucketParams.length; i++) {
72+
buckets[i] = Double.parseDouble(bucketParams[i]);
73+
}
74+
}
75+
}
76+
77+
requests = requestsBuilder.register();
78+
79+
if (buckets != null) {
80+
httpRequestDurationBuilder = httpRequestDurationBuilder.buckets(buckets);
81+
}
82+
83+
httpRequestDuration = httpRequestDurationBuilder.register();
84+
}
85+
86+
@Override
87+
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
88+
if (!(servletRequest instanceof HttpServletRequest)) {
89+
filterChain.doFilter(servletRequest, servletResponse);
90+
return;
91+
}
92+
93+
HttpServletRequest request = (HttpServletRequest) servletRequest;
94+
HttpServletResponse response = (HttpServletResponse) servletResponse;
95+
96+
String path = request.getRequestURI();
97+
String normalizedPath = extractPathFrom(path, pathDepth);
98+
99+
Histogram.Timer timer = httpRequestDuration
100+
.labels(normalizedPath, request.getMethod())
101+
.startTimer();
102+
try {
103+
filterChain.doFilter(servletRequest, servletResponse);
104+
requests.labels(normalizedPath, request.getMethod(), String.valueOf(response.getStatus())).inc();
105+
} finally {
106+
timer.observeDuration();
107+
}
108+
}
109+
110+
public String extractPathFrom(String requestUri, int maxPathDepth) {
111+
if (maxPathDepth < 0 || requestUri == null) {
112+
throw new IllegalArgumentException("Path depth has to >= 0");
113+
}
114+
115+
int count = 0;
116+
int pathPosition = -1;
117+
do {
118+
int lastPathPosition = pathPosition;
119+
pathPosition = requestUri.indexOf("/", pathPosition + 1);
120+
if (count > maxPathDepth || pathPosition < 0) {
121+
return requestUri.substring(0, lastPathPosition + 1);
122+
}
123+
count++;
124+
} while (count <= maxPathDepth);
125+
126+
return requestUri.substring(0, pathPosition + 1);
127+
}
128+
129+
@Override
130+
public void destroy() {
131+
}
132+
133+
}
134+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.gitblit.tests;
2+
3+
import com.gitblit.servlet.MetricsFilter;
4+
import org.junit.Test;
5+
6+
import static org.hamcrest.CoreMatchers.*;
7+
import static org.junit.Assert.*;
8+
9+
10+
public class MetricsFilterTest {
11+
12+
@Test
13+
public void alwaysExtractRootPathForZeroPathLength() {
14+
MetricsFilter metricsFilter = new MetricsFilter();
15+
String path = metricsFilter.extractPathFrom("/index.html", 0);
16+
assertThat(path, equalTo("/"));
17+
}
18+
19+
@Test
20+
public void useAlwaysRootPathForLongPathLength() {
21+
MetricsFilter metricsFilter = new MetricsFilter();
22+
String path = metricsFilter.extractPathFrom("/index.html", 1);
23+
assertThat(path, equalTo("/"));
24+
}
25+
26+
@Test
27+
public void pathDepthOneuseAlwaysRootPathForZeroPathLength() {
28+
MetricsFilter metricsFilter = new MetricsFilter();
29+
String path = metricsFilter.extractPathFrom("/test/index.html", 1);
30+
assertThat(path, equalTo("/test/"));
31+
}
32+
33+
@Test
34+
public void cutsPathsLongerThanPathDepth() {
35+
MetricsFilter metricsFilter = new MetricsFilter();
36+
String path = metricsFilter.extractPathFrom("/test/tralala/index.html", 1);
37+
assertThat(path, equalTo("/test/"));
38+
}
39+
40+
@Test(expected = IllegalArgumentException.class)
41+
public void throwsExceptionForNegativePathDepth() {
42+
new MetricsFilter().extractPathFrom("/index.html", -1);
43+
}
44+
45+
@Test(expected = IllegalArgumentException.class)
46+
public void throwsExceptionForNullRequestPath() {
47+
new MetricsFilter().extractPathFrom(null, 1);
48+
}
49+
50+
}

0 commit comments

Comments
 (0)