Skip to content

Commit 352b6b5

Browse files
andrewstuartbrian-brazil
authored andcommitted
Add AOP-based spring method timing metrics, servlet filter for path-based metrics. (prometheus#168)
Add servlet filter for timing requests and Spring AOP method timings
1 parent b49b8a9 commit 352b6b5

13 files changed

Lines changed: 998 additions & 6 deletions

File tree

README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,83 @@ or `EntityManagerFactory`, you can use this code to access the underlying `Sessi
400400
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
401401
```
402402

403+
#### Servlet Filter
404+
405+
There is a servlet filter available for measuring the duration taken by servlet
406+
requests. The `metric-name` init parameter is required, and is the name of the
407+
metric prometheus will expose for the timing metrics. Help text via the `help`
408+
init parameter is not required, although it is highly recommended. The number
409+
of buckets is overridable, and can be configured by passing a comma-separated
410+
string of doubles as the `buckets` init parameter. The granularity of path
411+
measuring is also configurable, via the `path-components` init parameter. By
412+
default, the servlet filter will record each path differently, but by setting an
413+
integer here, you can tell the filter to only record up to the Nth slashes. That
414+
is, all reqeusts with greater than N "/" characters in the servlet URI path will
415+
be measured in the same bucket and you will lose that granularity.
416+
417+
The code below is an example of the XML configuration for the filter. You will
418+
need to place this (replace your own values) code in your
419+
`webapp/WEB-INF/web.xml` file.
420+
421+
```xml
422+
<filter>
423+
<filter-name>prometheusFilter</filter-name>
424+
<filter-class>net.cccnext.ssp.portal.spring.filter.PrometheusMetricsFilter</filter-class>
425+
<init-param>
426+
<param-name>metric-name</param-name>
427+
<param-value>webapp_metrics_filter</param-value>
428+
</init-param>
429+
<init-param>
430+
<param-name>help</param-name>
431+
<param-value>This is the help for your metrics filter</param-value>
432+
</init-param>
433+
<init-param>
434+
<param-name>buckets</param-name>
435+
<param-value>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</param-value>
436+
</init-param>
437+
<!-- Optionally override path components; anything less than 1 (1 is the default)
438+
means full granularity -->
439+
<init-param>
440+
<param-name>path-components</param-name>
441+
<param-value>1</param-value>
442+
</init-param>
443+
</filter>
444+
445+
<!-- You will most likely want this to be the first filter in the chain
446+
(therefore the first <filter-mapping> in the web.xml file), so that you can get
447+
the most accurate measurement of latency. -->
448+
<filter-mapping>
449+
<filter-name>prometheusFilter</filter-name>
450+
<url-pattern>/*</url-pattern>
451+
</filter-mapping>
452+
```
453+
454+
Additionally, you can instantiate your servlet filter directly in Java code. To
455+
do this, you just need to call the non-empty constructor. The first parameter,
456+
the metric name, is required. The second, help, is optional but highly
457+
recommended. The last two (path-components, and buckets) are optional and will
458+
default sensibly if omitted.
459+
460+
#### Spring AOP
461+
462+
There is a Spring AOP collector that allows you to annotate methods that you
463+
would like to instrument with a [Summary](#Summary), but without going through
464+
the process of manually instaniating and registering your metrics classes. To
465+
use the metrics annotations, simply add `simpleclient_spring_web` as a
466+
dependency, annotate a configuration class with `@EnablePrometheusTiming`, then
467+
annotate your Spring components as such:
468+
469+
```java
470+
@Controller
471+
public class MyController {
472+
@RequestMapping("/")
473+
@PrometheusTimeMethod(name = "my_controller_path_duration_seconds", help = "Some helpful info here")
474+
public Object handleMain() {
475+
// Do something
476+
}
477+
}
478+
```
479+
403480
## Exporting
404481

405482
There are several options for exporting metrics.

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
<module>simpleclient_logback</module>
5757
<module>simpleclient_pushgateway</module>
5858
<module>simpleclient_servlet</module>
59+
<module>simpleclient_spring_web</module>
5960
<module>simpleclient_spring_boot</module>
6061
<module>simpleclient_jetty</module>
6162
<module>simpleclient_vertx</module>

simpleclient_servlet/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,11 @@
7373
<version>1.9.5</version>
7474
<scope>test</scope>
7575
</dependency>
76+
<dependency>
77+
<groupId>org.mockito</groupId>
78+
<artifactId>mockito-core</artifactId>
79+
<version>1.9.5</version>
80+
<scope>test</scope>
81+
</dependency>
7682
</dependencies>
7783
</project>
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package io.prometheus.client.filter;
2+
3+
import io.prometheus.client.Histogram;
4+
5+
import javax.servlet.Filter;
6+
import javax.servlet.FilterChain;
7+
import javax.servlet.FilterConfig;
8+
import javax.servlet.ServletException;
9+
import javax.servlet.ServletRequest;
10+
import javax.servlet.ServletResponse;
11+
import javax.servlet.http.HttpServletRequest;
12+
import java.io.IOException;
13+
14+
/**
15+
* The MetricsFilter class exists to provide a high-level filter that enables tunable collection of metrics for Servlet
16+
* performance.
17+
*
18+
* The Histogram name itself is required, and configured with a {@code metric-name} init parameter.
19+
*
20+
* The help parameter, configured with the {@code help} init parameter, is not required but strongly recommended.
21+
*
22+
* By default, this filter will provide metrics that distinguish only 1 level deep for the request path
23+
* (including servlet context path), but can be configured with the {@code path-components} init parameter. Any number
24+
* provided that is less than 1 will provide the full path granularity (warning, this may affect performance).
25+
*
26+
* The Histogram buckets can be configured with a {@code buckets} init parameter whose value is a comma-separated list
27+
* of valid {@code double} values.
28+
*
29+
* {@code
30+
* <filter>
31+
* <filter-name>prometheusFilter</filter-name>
32+
* <filter-class>net.cccnext.ssp.portal.spring.filter.PrometheusMetricsFilter</filter-class>
33+
* <init-param>
34+
* <param-name>metric-name</param-name>
35+
* <param-value>webapp_metrics_filter</param-value>
36+
* </init-param>
37+
* <init-param>
38+
* <param-name>help</param-name>
39+
* <param-value>The time taken fulfilling servlet requests</param-value>
40+
* </init-param>
41+
* <init-param>
42+
* <param-name>buckets</param-name>
43+
* <param-value>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</param-value>
44+
* </init-param>
45+
* <init-param>
46+
* <param-name>path-components</param-name>
47+
* <param-value>0</param-value>
48+
* </init-param>
49+
* </filter>
50+
* }
51+
*
52+
* @author Andrew Stuart &lt;andrew.stuart2@gmail.com&gt;
53+
*/
54+
public class MetricsFilter implements Filter {
55+
static final String PATH_COMPONENT_PARAM = "path-components";
56+
static final String HELP_PARAM = "help";
57+
static final String METRIC_NAME_PARAM = "metric-name";
58+
static final String BUCKET_CONFIG_PARAM = "buckets";
59+
60+
private Histogram histogram = null;
61+
62+
// Package-level for testing purposes.
63+
int pathComponents = 1;
64+
private String metricName = null;
65+
private String help = "The time taken fulfilling servlet requests";
66+
private double[] buckets = null;
67+
68+
public MetricsFilter() {}
69+
70+
public MetricsFilter(
71+
String metricName,
72+
String help,
73+
Integer pathComponents,
74+
double[] buckets
75+
) throws ServletException {
76+
this.metricName = metricName;
77+
this.buckets = buckets;
78+
if (help != null) {
79+
this.help = help;
80+
}
81+
if (pathComponents != null) {
82+
this.pathComponents = pathComponents;
83+
}
84+
this.init(null);
85+
}
86+
87+
private boolean isEmpty(String s) {
88+
return s == null || s.length() == 0;
89+
}
90+
91+
private String getComponents(String str) {
92+
if (str == null || pathComponents < 1) {
93+
return str;
94+
}
95+
int count = 0;
96+
int i = -1;
97+
do {
98+
i = str.indexOf("/", i + 1);
99+
if (i < 0) {
100+
// Path is longer than specified pathComponents.
101+
return str;
102+
}
103+
count++;
104+
} while (count <= pathComponents);
105+
106+
return str.substring(0, i);
107+
}
108+
109+
@Override
110+
public void init(FilterConfig filterConfig) throws ServletException {
111+
Histogram.Builder builder = Histogram.build()
112+
.labelNames("path", "method");
113+
114+
if (filterConfig == null && isEmpty(metricName)) {
115+
throw new ServletException("No configuration object provided, and no metricName passed via constructor");
116+
}
117+
118+
if (filterConfig != null) {
119+
if (isEmpty(metricName)) {
120+
metricName = filterConfig.getInitParameter(METRIC_NAME_PARAM);
121+
if (isEmpty(metricName)) {
122+
throw new ServletException("Init parameter \"" + METRIC_NAME_PARAM + "\" is required; please supply a value");
123+
}
124+
}
125+
126+
if (!isEmpty(filterConfig.getInitParameter(HELP_PARAM))) {
127+
help = filterConfig.getInitParameter(HELP_PARAM);
128+
}
129+
130+
// Allow overriding of the path "depth" to track
131+
if (!isEmpty(filterConfig.getInitParameter(PATH_COMPONENT_PARAM))) {
132+
pathComponents = Integer.valueOf(filterConfig.getInitParameter(PATH_COMPONENT_PARAM));
133+
}
134+
135+
// Allow users to override the default bucket configuration
136+
if (!isEmpty(filterConfig.getInitParameter(BUCKET_CONFIG_PARAM))) {
137+
String[] bucketParams = filterConfig.getInitParameter(BUCKET_CONFIG_PARAM).split(",");
138+
buckets = new double[bucketParams.length];
139+
140+
for (int i = 0; i < bucketParams.length; i++) {
141+
buckets[i] = Double.parseDouble(bucketParams[i]);
142+
}
143+
}
144+
}
145+
146+
if (buckets != null) {
147+
builder = builder.buckets(buckets);
148+
}
149+
150+
histogram = builder
151+
.help(help)
152+
.name(metricName)
153+
.register();
154+
}
155+
156+
@Override
157+
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
158+
if (!(servletRequest instanceof HttpServletRequest)) {
159+
filterChain.doFilter(servletRequest, servletResponse);
160+
return;
161+
}
162+
163+
HttpServletRequest request = (HttpServletRequest) servletRequest;
164+
165+
String path = request.getRequestURI();
166+
167+
Histogram.Timer timer = histogram
168+
.labels(getComponents(path), request.getMethod())
169+
.startTimer();
170+
171+
try {
172+
filterChain.doFilter(servletRequest, servletResponse);
173+
} finally {
174+
timer.observeDuration();
175+
}
176+
}
177+
178+
@Override
179+
public void destroy() {
180+
}
181+
}

0 commit comments

Comments
 (0)