diff --git a/.classpath b/.classpath
index 42cbfbc87..1ddd14a99 100644
--- a/.classpath
+++ b/.classpath
@@ -80,6 +80,12 @@
+
+
+
+
+
+
diff --git a/build.moxie b/build.moxie
index f21241d1b..d6880b0ba 100644
--- a/build.moxie
+++ b/build.moxie
@@ -181,6 +181,11 @@ dependencies:
- compile 'ro.fortsoft.pf4j:pf4j:0.9.0' :war
- compile 'org.apache.tika:tika-core:1.5' :war
- compile 'org.jsoup:jsoup:1.7.3' :war
+- compile 'io.prometheus:simpleclient:0.0.21' :war
+- compile 'io.prometheus:simpleclient_hotspot:0.0.21' :war
+- compile 'io.prometheus:simpleclient_log4j:0.0.21' :war
+- compile 'io.prometheus:simpleclient_guava:0.0.21' :war
+- compile 'io.prometheus:simpleclient_servlet:0.0.21' :war
- test 'junit:junit:4.12'
# Dependencies for Selenium web page testing
- test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar
diff --git a/gitblit.iml b/gitblit.iml
index 1f4aa2483..b25cffbeb 100644
--- a/gitblit.iml
+++ b/gitblit.iml
@@ -824,6 +824,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/gitblit/guice/WebModule.java b/src/main/java/com/gitblit/guice/WebModule.java
index 7c83e455b..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,8 +44,13 @@
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;
+import io.prometheus.client.hotspot.DefaultExports;
/**
* Defines all the web servlets & filters.
@@ -79,6 +85,12 @@ protected void configureServlets() {
serve("/robots.txt").with(RobotsTxtServlet.class);
serve("/logo.png").with(LogoServlet.class);
+ // Prometheus
+ bind(MetricsServlet.class).in(Scopes.SINGLETON);
+ bind(MetricsFilter.class).in(Scopes.SINGLETON);
+ serve("/prometheus").with(MetricsServlet.class);
+ DefaultExports.initialize();
+
/* Prevent accidental access to 'resources' such as GitBlit java classes
*
* In the GO setup the JAR containing the application and the WAR injected
@@ -91,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/service/GarbageCollectorService.java b/src/main/java/com/gitblit/service/GarbageCollectorService.java
index b98560fd9..fce5c5112 100644
--- a/src/main/java/com/gitblit/service/GarbageCollectorService.java
+++ b/src/main/java/com/gitblit/service/GarbageCollectorService.java
@@ -23,6 +23,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
+import io.prometheus.client.Counter;
import org.eclipse.jgit.api.GarbageCollectCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Repository;
@@ -35,6 +36,8 @@
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.FileUtils;
+import static com.gitblit.service.PrometheusMetrics.GIT_GARBAGE_COLLECTS_TOTAL;
+
/**
* The Garbage Collector Service handles periodic garbage collection in repositories.
*
@@ -129,6 +132,9 @@ public void close() {
forceClose.set(true);
}
+ private final Counter garbageCollectsTotal = Counter.build()
+ .name(GIT_GARBAGE_COLLECTS_TOTAL).help(GIT_GARBAGE_COLLECTS_TOTAL).register();
+
@Override
public void run() {
if (!isReady()) {
@@ -200,7 +206,7 @@ public void run() {
// do the deed
gc.call();
-
+ garbageCollectsTotal.inc();
garbageCollected = true;
}
} catch (Exception e) {
diff --git a/src/main/java/com/gitblit/service/LdapSyncService.java b/src/main/java/com/gitblit/service/LdapSyncService.java
index 7ae19aad8..4b1cb04ff 100644
--- a/src/main/java/com/gitblit/service/LdapSyncService.java
+++ b/src/main/java/com/gitblit/service/LdapSyncService.java
@@ -17,6 +17,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
+import io.prometheus.client.Histogram;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -24,6 +25,8 @@
import com.gitblit.Keys;
import com.gitblit.auth.LdapAuthProvider;
+import static com.gitblit.service.PrometheusMetrics.LDAP_SYNC_LATENCY_SECONDS;
+
/**
* @author Alfred Schmid
*
@@ -31,6 +34,8 @@
public final class LdapSyncService implements Runnable {
private final Logger logger = LoggerFactory.getLogger(LdapSyncService.class);
+ private final Histogram ldapSyncLatency = Histogram.build().name(LDAP_SYNC_LATENCY_SECONDS).
+ help(LDAP_SYNC_LATENCY_SECONDS).register();
private final IStoredSettings settings;
@@ -51,12 +56,14 @@ public LdapSyncService(IStoredSettings settings, LdapAuthProvider ldapAuthProvid
public void run() {
logger.info("Starting user and group sync with ldap service");
if (!running.getAndSet(true)) {
+ Histogram.Timer requestTimer = ldapSyncLatency.startTimer();
try {
ldapAuthProvider.sync();
} catch (Exception e) {
logger.error("Failed to synchronize with ldap", e);
} finally {
running.getAndSet(false);
+ requestTimer.observeDuration();
}
}
logger.info("Finished user and group sync with ldap service");
diff --git a/src/main/java/com/gitblit/service/PrometheusMetrics.java b/src/main/java/com/gitblit/service/PrometheusMetrics.java
new file mode 100644
index 000000000..53c4cbd9e
--- /dev/null
+++ b/src/main/java/com/gitblit/service/PrometheusMetrics.java
@@ -0,0 +1,10 @@
+package com.gitblit.service;
+
+class PrometheusMetrics {
+
+ /** Number of garbage collects */
+ static final String GIT_GARBAGE_COLLECTS_TOTAL = "gitblit_garbage_collects_total";
+
+ /** Ldap Sync Latency in second */
+ static final String LDAP_SYNC_LATENCY_SECONDS = "gitblit_ldap_sync_latency_seconds";
+}
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/main/java/log4j.properties b/src/main/java/log4j.properties
index 43d31d80b..f315fa084 100644
--- a/src/main/java/log4j.properties
+++ b/src/main/java/log4j.properties
@@ -17,7 +17,7 @@
# FATAL, ERROR, WARN, INFO, DEBUG
#
#------------------------------------------------------------------------------
-log4j.rootCategory=INFO, S
+log4j.rootCategory=INFO, S, METRICS
#log4j.rootLogger=INFO
#log4j.logger.org=INFO
@@ -67,3 +67,6 @@ log4j.appender.H.File = logs/gitblit.html
log4j.appender.H.MaxFileSize = 100KB
log4j.appender.H.Append = false
log4j.appender.H.layout = org.apache.log4j.HTMLLayout
+
+log4j.appender.METRICS = io.prometheus.client.log4j.InstrumentedAppender
+log4j.appender.METRICS.Append = false
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