diff --git a/simpleclient/src/main/java/io/prometheus/client/SimpleCollector.java b/simpleclient/src/main/java/io/prometheus/client/SimpleCollector.java
index 2f10f0730..d3dd2d768 100644
--- a/simpleclient/src/main/java/io/prometheus/client/SimpleCollector.java
+++ b/simpleclient/src/main/java/io/prometheus/client/SimpleCollector.java
@@ -11,7 +11,7 @@
* This class handles common initialization and label logic for the standard metrics.
* You should never subclass this class.
*
- *
Initilization
+ * Initialization
* After calling build() on a subclass, {@link Builder#name(String) name},
* {@link SimpleCollector.Builder#help(String) help},
* {@link SimpleCollector.Builder#labelNames(String...) labelNames},
diff --git a/simpleclient_servlet/pom.xml b/simpleclient_servlet/pom.xml
index f0a0ae40a..a13b6ca5c 100644
--- a/simpleclient_servlet/pom.xml
+++ b/simpleclient_servlet/pom.xml
@@ -67,5 +67,10 @@
8.1.7.v20120910
test
+
+ org.apache.commons
+ commons-lang3
+ 3.4
+
diff --git a/simpleclient_servlet/src/main/java/io/prometheus/client/filter/MetricsFilter.java b/simpleclient_servlet/src/main/java/io/prometheus/client/filter/MetricsFilter.java
new file mode 100644
index 000000000..6b8443d55
--- /dev/null
+++ b/simpleclient_servlet/src/main/java/io/prometheus/client/filter/MetricsFilter.java
@@ -0,0 +1,77 @@
+package io.prometheus.client.filter;
+
+import io.prometheus.client.Counter;
+import io.prometheus.client.Histogram;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Created by andrewstuart on 11/19/16.
+ *
+ * The MetricsFilter class exists to provide a high-level filter that enables tunable collection of metrics for Servlet
+ * performance.
+ *
+ * By default, this filter will provide metrics that distinguish 3 levels deep for the request path
+ * (including servlet context path).
+ */
+public class MetricsFilter implements Filter {
+ public static final String PATH_COMPONENT_PARAM = "path-components";
+ public static final int DEFAULT_PATH_COMPONENTS = 3;
+
+ private static Histogram servletLatency = null;
+
+ private static int pathComponents = DEFAULT_PATH_COMPONENTS;
+
+ private static boolean isEmpty(String string) {
+ return string != null && !"".equals(string);
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ servletLatency = Histogram.build()
+ .name("servlet_request_latency")
+ .help("The time taken fulfilling uportal requests")
+ .labelNames("path", "verb")
+ .register();
+
+ if (!isEmpty(filterConfig.getInitParameter(PATH_COMPONENT_PARAM))) {
+ pathComponents = Integer.valueOf(filterConfig.getInitParameter(PATH_COMPONENT_PARAM));
+ }
+ }
+
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+ if (servletLatency == null || !(servletRequest instanceof HttpServletRequest)) {
+ filterChain.doFilter(servletRequest, servletResponse);
+ return;
+ }
+
+ HttpServletRequest request = (HttpServletRequest) servletRequest;
+
+ String path = request.getRequestURI();
+ int lastSlash = StringUtils.ordinalIndexOf(path, "/", pathComponents+1);
+
+ Histogram.Timer timer = servletLatency
+ .labels(lastSlash == -1 ? path : path.substring(0, lastSlash), request.getMethod())
+ .startTimer();
+
+ try {
+ filterChain.doFilter(servletRequest, servletResponse);
+ } finally {
+ timer.observeDuration();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/simpleclient_spring_boot/pom.xml b/simpleclient_spring_boot/pom.xml
index be308780b..9b4b5c58f 100644
--- a/simpleclient_spring_boot/pom.xml
+++ b/simpleclient_spring_boot/pom.xml
@@ -69,6 +69,16 @@
spring-boot-actuator
1.3.3.RELEASE
+
+ org.springframework.boot
+ spring-boot-starter-aop
+ 1.3.3.RELEASE
+
+
+ org.apache.commons
+ commons-lang3
+ 3.4
+
diff --git a/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnablePrometheusEndpoint.java b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnablePrometheusEndpoint.java
index e799756e9..b11bd747f 100644
--- a/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnablePrometheusEndpoint.java
+++ b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnablePrometheusEndpoint.java
@@ -38,7 +38,7 @@
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
-@Import(PrometheusEndpointConfiguration.class)
+@Import({PrometheusEndpointConfiguration.class, MethodTimer.class})
public @interface EnablePrometheusEndpoint {
}
diff --git a/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnablePrometheusTiming.java b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnablePrometheusTiming.java
new file mode 100644
index 000000000..116039d1c
--- /dev/null
+++ b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnablePrometheusTiming.java
@@ -0,0 +1,17 @@
+package io.prometheus.client.spring.boot;
+
+import org.springframework.context.annotation.Import;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Created by andrewstuart on 11/26/16.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Import(MethodTimer.class)
+public @interface EnablePrometheusTiming {
+}
\ No newline at end of file
diff --git a/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnableSpringBootMetricsCollector.java b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnableSpringBootMetricsCollector.java
index c9a52850c..ddd204a5b 100644
--- a/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnableSpringBootMetricsCollector.java
+++ b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnableSpringBootMetricsCollector.java
@@ -12,6 +12,4 @@
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(PrometheusMetricsConfiguration.class)
-public @interface EnableSpringBootMetricsCollector {
-
-}
+public @interface EnableSpringBootMetricsCollector {}
diff --git a/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/MethodTimer.java b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/MethodTimer.java
new file mode 100644
index 000000000..1584150dc
--- /dev/null
+++ b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/MethodTimer.java
@@ -0,0 +1,40 @@
+package io.prometheus.client.spring.boot;
+
+import io.prometheus.client.Histogram;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+
+import org.springframework.context.annotation.Import;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+
+/**
+ * Created by andrew on 11/24/16.
+ */
+@Aspect
+@ControllerAdvice
+@Component
+public class MethodTimer {
+ public static final String METRIC_NAME = "prometheus_method_timing";
+
+ public static final Histogram hist = Histogram.build()
+ .name(METRIC_NAME)
+ .help("Automatic method timing")
+ .labelNames("signature")
+ .register();
+
+ @Around("within(@PrometheusMethodTiming *) || execution(@PrometheusMethodTiming * *.*(..))")
+ public Object timeMethod(ProceedingJoinPoint pjp) throws Throwable {
+ Histogram.Timer t = hist.labels(pjp.getSignature().toShortString()).startTimer();
+ try {
+ Object result = pjp.proceed();
+ t.observeDuration();
+ return result;
+ } catch (Throwable exception) {
+ t.observeDuration();
+ throw exception;
+ }
+ }
+}
diff --git a/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/PrometheusMethodTiming.java b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/PrometheusMethodTiming.java
new file mode 100644
index 000000000..e97be4e28
--- /dev/null
+++ b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/PrometheusMethodTiming.java
@@ -0,0 +1,16 @@
+package io.prometheus.client.spring.boot;
+
+import org.springframework.context.annotation.Import;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Created by andrew on 11/24/16.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface PrometheusMethodTiming {
+}
diff --git a/simpleclient_spring_boot/src/test/java/io/prometheus/client/annotation/AutomaticMethodTimerTest.java b/simpleclient_spring_boot/src/test/java/io/prometheus/client/annotation/AutomaticMethodTimerTest.java
new file mode 100644
index 000000000..0887268b4
--- /dev/null
+++ b/simpleclient_spring_boot/src/test/java/io/prometheus/client/annotation/AutomaticMethodTimerTest.java
@@ -0,0 +1,89 @@
+package io.prometheus.client.annotation;
+
+import io.prometheus.client.Collector;
+import io.prometheus.client.spring.boot.MethodTimer;
+import io.prometheus.client.spring.boot.PrometheusMethodTiming;
+import org.junit.Test;
+import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
+
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * Created by andrew on 11/25/16.
+ */
+public class AutomaticMethodTimerTest {
+ Timeable proxy;
+
+ private static interface Timeable {
+ public void timeMe() throws Exception;
+ }
+
+ private static class TestClass implements Timeable {
+ @PrometheusMethodTiming
+ public void timeMe() throws Exception {
+ Thread.sleep(20);
+ }
+ }
+
+ private static interface Time2 {
+ public void timeMe() throws Exception;
+ }
+
+ @PrometheusMethodTiming
+ private static class TestClass2 implements Time2 {
+ public void timeMe() throws Exception {
+ Thread.sleep(30);
+ }
+ }
+
+ private TestClass c;
+
+ @Test
+ public void timeMethod() throws Exception {
+ Timeable cprime = new TestClass();
+ AspectJProxyFactory factory = new AspectJProxyFactory(cprime);
+ MethodTimer timer = new MethodTimer();
+ factory.addAspect(timer);
+ Timeable proxy = factory.getProxy();
+
+ proxy.timeMe();
+
+ final List samples = MethodTimer.hist.collect();
+
+ assertNotNull(samples);
+ assertEquals(samples.size(), 1);
+ double tot = 0.0;
+ for(Collector.MetricFamilySamples.Sample s : samples.get(0).samples) {
+ if (s.name.equals(MethodTimer.METRIC_NAME + "_sum") && s.labelValues.get(0).equals("Timeable.timeMe()")) {
+ tot = s.value;
+ }
+ }
+ System.out.print(tot);
+ assert(0.019 < tot && tot < 0.025);
+ }
+
+ @Test
+ public void timeClassAnnotation() throws Exception {
+ Time2 cprime = new TestClass2();
+ AspectJProxyFactory factory = new AspectJProxyFactory(cprime);
+ MethodTimer timer = new MethodTimer();
+ factory.addAspect(timer);
+ Time2 proxy = factory.getProxy();
+
+ proxy.timeMe();
+
+ final List samples = MethodTimer.hist.collect();
+
+ assertNotNull(samples);
+ assertEquals(samples.size(), 1);
+ double tot = 0.0;
+ for(Collector.MetricFamilySamples.Sample s : samples.get(0).samples) {
+ if (s.name.equals(MethodTimer.METRIC_NAME + "_sum") && s.labelValues.get(0).equals("Time2.timeMe()")) {
+ tot = s.value;
+ }
+ }
+ assert(0.029 < tot && tot < 0.035);
+ }
+}
\ No newline at end of file