Skip to content

Commit 2671182

Browse files
committed
spring-projectsgh-5875 fix for Jersey templates in actuator metrics
1 parent 6b86190 commit 2671182

File tree

5 files changed

+223
-50
lines changed

5 files changed

+223
-50
lines changed

spring-boot-actuator/pom.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,11 @@
284284
<artifactId>spring-boot-configuration-processor</artifactId>
285285
<optional>true</optional>
286286
</dependency>
287+
<dependency>
288+
<groupId>org.glassfish.jersey.core</groupId>
289+
<artifactId>jersey-server</artifactId>
290+
<optional>true</optional>
291+
</dependency>
287292
<!-- Test -->
288293
<dependency>
289294
<groupId>org.springframework.boot</groupId>
@@ -383,5 +388,26 @@
383388
<artifactId>snakeyaml</artifactId>
384389
<scope>test</scope>
385390
</dependency>
391+
<dependency>
392+
<groupId>org.glassfish.jersey.containers</groupId>
393+
<artifactId>jersey-container-servlet-core</artifactId>
394+
<scope>test</scope>
395+
</dependency>
396+
<dependency>
397+
<groupId>org.glassfish.jersey.containers</groupId>
398+
<artifactId>jersey-container-servlet</artifactId>
399+
<scope>test</scope>
400+
</dependency>
401+
<dependency>
402+
<groupId>org.glassfish.jersey.ext</groupId>
403+
<artifactId>jersey-spring3</artifactId>
404+
<scope>test</scope>
405+
<exclusions>
406+
<exclusion>
407+
<groupId>org.jvnet</groupId>
408+
<artifactId>tiger-types</artifactId>
409+
</exclusion>
410+
</exclusions>
411+
</dependency>
386412
</dependencies>
387413
</project>

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricsFilter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ private String determineMetricNameSuffix(HttpServletRequest request, String path
152152
if (bestMatchingPattern != null) {
153153
return fixSpecialCharacters(bestMatchingPattern.toString());
154154
}
155+
//TODO static final somewhere
156+
bestMatchingPattern = request.getAttribute("JERSEY_TEMPLATE");
157+
if (bestMatchingPattern != null) {
158+
return fixSpecialCharacters(bestMatchingPattern.toString());
159+
}
155160
Series series = getSeries(status);
156161
if (Series.CLIENT_ERROR.equals(series) || Series.SERVER_ERROR.equals(series)
157162
|| Series.REDIRECTION.equals(series)) {

spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
4848
import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration;
4949
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
50-
import org.springframework.boot.context.embedded.LocalServerPort;
5150
import org.springframework.boot.test.context.SpringBootTest;
5251
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
5352
import org.springframework.boot.test.web.client.TestRestTemplate;
@@ -77,24 +76,22 @@
7776
@TestPropertySource(properties = "management.security.enabled=false")
7877
public class EndpointMvcIntegrationTests {
7978

80-
@LocalServerPort
81-
private int port;
82-
8379
@Autowired
8480
private TestInterceptor interceptor;
8581

82+
@Autowired
83+
private TestRestTemplate testRestTemplate;
84+
8685
@Test
8786
public void envEndpointNotHidden() throws InterruptedException {
88-
String body = new TestRestTemplate().getForObject(
89-
"http://localhost:" + this.port + "/env/user.dir", String.class);
87+
String body = testRestTemplate.getForObject("/env/user.dir", String.class);
9088
assertThat(body).isNotNull().contains("spring-boot-actuator");
9189
assertThat(this.interceptor.invoked()).isTrue();
9290
}
9391

9492
@Test
9593
public void healthEndpointNotHidden() throws InterruptedException {
96-
String body = new TestRestTemplate()
97-
.getForObject("http://localhost:" + this.port + "/health", String.class);
94+
String body = testRestTemplate.getForObject("/health", String.class);
9895
assertThat(body).isNotNull().contains("status");
9996
assertThat(this.interceptor.invoked()).isTrue();
10097
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package org.springframework.boot.actuate.autoconfigure;
2+
3+
import org.glassfish.jersey.server.ResourceConfig;
4+
import org.junit.Test;
5+
import org.junit.runner.RunWith;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.boot.actuate.metrics.CounterService;
8+
import org.springframework.boot.actuate.metrics.GaugeService;
9+
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
10+
import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration;
11+
import org.springframework.boot.test.context.SpringBootTest;
12+
import org.springframework.boot.test.web.client.TestRestTemplate;
13+
import org.springframework.context.annotation.Import;
14+
import org.springframework.stereotype.Component;
15+
import org.springframework.test.annotation.DirtiesContext;
16+
import org.springframework.test.context.TestPropertySource;
17+
import org.springframework.test.context.junit4.SpringRunner;
18+
19+
import javax.ws.rs.ApplicationPath;
20+
import javax.ws.rs.GET;
21+
import javax.ws.rs.Path;
22+
import javax.ws.rs.PathParam;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.mockito.Matchers.anyDouble;
26+
import static org.mockito.Matchers.eq;
27+
import static org.mockito.Mockito.verify;
28+
29+
@RunWith(SpringRunner.class)
30+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
31+
@DirtiesContext
32+
@TestPropertySource(properties = "management.security.enabled=false")
33+
public class MetricFilterJerseyTests {
34+
35+
@Autowired
36+
private TestRestTemplate testRestTemplate;
37+
38+
@Autowired
39+
private CounterService counterService;
40+
41+
@Autowired
42+
private GaugeService gaugeService;
43+
44+
@Test
45+
public void recordsHttpInteractionsWithTemplateVariables() {
46+
String body = testRestTemplate.getForObject("/api/templateVarTest/foo", String.class);
47+
assertThat(body).isEqualTo("foo");
48+
49+
body = testRestTemplate.getForObject("/api/templateRegexpTest/foo", String.class);
50+
assertThat(body).isEqualTo("foo");
51+
52+
verify(counterService).increment("status.200.api.templateVarTest.someVariable");
53+
verify(gaugeService).submit(eq("response.api.templateVarTest.someVariable"), anyDouble());
54+
55+
verify(counterService).increment("status.200.api.templateRegexpTest.someRegexp");
56+
verify(gaugeService).submit(eq("response.api.templateRegexpTest.someRegexp"), anyDouble());
57+
}
58+
59+
@MinimalActuatorHypermediaApplication
60+
@ImportAutoConfiguration({JerseyAutoConfiguration.class, MetricFilterAutoConfiguration.class})
61+
@Import({JerseyConfig.class, MetricFilterAutoConfigurationTests.Config.class})
62+
public static class Application {
63+
}
64+
65+
@Component
66+
@ApplicationPath("/api")
67+
public static class JerseyConfig extends ResourceConfig {
68+
public JerseyConfig() {
69+
register(TemplateVarEndpoint.class);
70+
register(TemplateRegexpEndpoint.class);
71+
}
72+
}
73+
74+
@Path("/templateVarTest/{someVariable}")
75+
public static class TemplateVarEndpoint {
76+
@GET
77+
public String get(@PathParam("someVariable") String someVariable) {
78+
return someVariable;
79+
}
80+
}
81+
82+
@Path("/templateRegexpTest/{someRegexp: [a-zA-Z]*}")
83+
public static class TemplateRegexpEndpoint {
84+
@GET
85+
public String get(@PathParam("someRegexp") String someRegexp) {
86+
return someRegexp;
87+
}
88+
}
89+
90+
}

spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java

Lines changed: 97 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,6 @@
1616

1717
package org.springframework.boot.autoconfigure.jersey;
1818

19-
import java.util.Arrays;
20-
import java.util.EnumSet;
21-
import java.util.List;
22-
import java.util.Map.Entry;
23-
24-
import javax.annotation.PostConstruct;
25-
import javax.servlet.DispatcherType;
26-
import javax.servlet.ServletContext;
27-
import javax.servlet.ServletException;
28-
import javax.servlet.ServletRegistration;
29-
import javax.ws.rs.ApplicationPath;
30-
import javax.ws.rs.ext.ContextResolver;
31-
3219
import com.fasterxml.jackson.databind.AnnotationIntrospector;
3320
import com.fasterxml.jackson.databind.ObjectMapper;
3421
import com.fasterxml.jackson.databind.cfg.MapperConfig;
@@ -38,20 +25,16 @@
3825
import org.glassfish.jersey.CommonProperties;
3926
import org.glassfish.jersey.jackson.JacksonFeature;
4027
import org.glassfish.jersey.server.ResourceConfig;
28+
import org.glassfish.jersey.server.internal.routing.UriRoutingContext;
4129
import org.glassfish.jersey.servlet.ServletContainer;
4230
import org.glassfish.jersey.servlet.ServletProperties;
43-
31+
import org.glassfish.jersey.uri.UriTemplate;
4432
import org.springframework.beans.factory.ObjectProvider;
4533
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
4634
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
4735
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
4836
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
49-
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
50-
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
51-
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
52-
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
53-
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
54-
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
37+
import org.springframework.boot.autoconfigure.condition.*;
5538
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
5639
import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration;
5740
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -70,6 +53,21 @@
7053
import org.springframework.web.context.ServletContextAware;
7154
import org.springframework.web.filter.RequestContextFilter;
7255

56+
import javax.annotation.PostConstruct;
57+
import javax.servlet.DispatcherType;
58+
import javax.servlet.ServletContext;
59+
import javax.servlet.ServletException;
60+
import javax.servlet.ServletRegistration;
61+
import javax.ws.rs.ApplicationPath;
62+
import javax.ws.rs.container.ContainerRequestContext;
63+
import javax.ws.rs.container.ContainerRequestFilter;
64+
import javax.ws.rs.core.UriInfo;
65+
import javax.ws.rs.ext.ContextResolver;
66+
import java.util.Collections;
67+
import java.util.EnumSet;
68+
import java.util.List;
69+
import java.util.Map.Entry;
70+
7371
/**
7472
* {@link EnableAutoConfiguration Auto-configuration} for Jersey.
7573
*
@@ -95,34 +93,20 @@ public class JerseyAutoConfiguration implements ServletContextAware {
9593

9694
private final ResourceConfig config;
9795

98-
private final List<ResourceConfigCustomizer> customizers;
96+
private final JerseyPathResolver pathResolver;
9997

100-
private String path;
98+
private final List<ResourceConfigCustomizer> customizers;
10199

102100
public JerseyAutoConfiguration(JerseyProperties jersey, ResourceConfig config,
103-
ObjectProvider<List<ResourceConfigCustomizer>> customizers) {
101+
JerseyPathResolver pathResolver, ObjectProvider<List<ResourceConfigCustomizer>> customizers) {
104102
this.jersey = jersey;
105103
this.config = config;
104+
this.pathResolver = pathResolver;
106105
this.customizers = customizers.getIfAvailable();
107106
}
108107

109108
@PostConstruct
110-
public void path() {
111-
resolveApplicationPath();
112-
customize();
113-
}
114-
115-
private void resolveApplicationPath() {
116-
if (StringUtils.hasLength(this.jersey.getApplicationPath())) {
117-
this.path = parseApplicationPath(this.jersey.getApplicationPath());
118-
}
119-
else {
120-
this.path = findApplicationPath(AnnotationUtils.findAnnotation(
121-
this.config.getApplication().getClass(), ApplicationPath.class));
122-
}
123-
}
124-
125-
private void customize() {
109+
public void customize() {
126110
if (this.customizers != null) {
127111
AnnotationAwareOrderComparator.sort(this.customizers);
128112
for (ResourceConfigCustomizer customizer : this.customizers) {
@@ -147,10 +131,10 @@ public FilterRegistrationBean requestContextFilter() {
147131
public FilterRegistrationBean jerseyFilterRegistration() {
148132
FilterRegistrationBean registration = new FilterRegistrationBean();
149133
registration.setFilter(new ServletContainer(this.config));
150-
registration.setUrlPatterns(Arrays.asList(this.path));
134+
registration.setUrlPatterns(Collections.singletonList(this.pathResolver.getPath()));
151135
registration.setOrder(this.jersey.getFilter().getOrder());
152136
registration.addInitParameter(ServletProperties.FILTER_CONTEXT_PATH,
153-
stripPattern(this.path));
137+
stripPattern(this.pathResolver.getPath()));
154138
addInitParameters(registration);
155139
registration.setName("jerseyFilter");
156140
registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
@@ -169,7 +153,7 @@ private String stripPattern(String path) {
169153
@ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "servlet", matchIfMissing = true)
170154
public ServletRegistrationBean jerseyServletRegistration() {
171155
ServletRegistrationBean registration = new ServletRegistrationBean(
172-
new ServletContainer(this.config), this.path);
156+
new ServletContainer(this.config), this.pathResolver.getPath());
173157
addInitParameters(registration);
174158
registration.setName(getServletRegistrationName());
175159
registration.setLoadOnStartup(this.jersey.getServlet().getLoadOnStartup());
@@ -231,6 +215,77 @@ public void onStartup(ServletContext servletContext) throws ServletException {
231215

232216
}
233217

218+
219+
@Configuration
220+
static class JerseyPathResolver {
221+
private final JerseyProperties jersey;
222+
223+
private final ResourceConfig config;
224+
225+
private String path;
226+
227+
JerseyPathResolver(JerseyProperties jersey, ResourceConfig config) {
228+
this.jersey = jersey;
229+
this.config = config;
230+
}
231+
232+
public String getPath() {
233+
return path;
234+
}
235+
236+
@PostConstruct
237+
public void resolveApplicationPath() {
238+
if (StringUtils.hasLength(this.jersey.getApplicationPath())) {
239+
this.path = parseApplicationPath(this.jersey.getApplicationPath());
240+
}
241+
else {
242+
this.path = findApplicationPath(AnnotationUtils.findAnnotation(
243+
this.config.getApplication().getClass(), ApplicationPath.class));
244+
}
245+
}
246+
}
247+
248+
@Configuration
249+
// @ConditionalOnBean(CounterService.class) //TODO some Conditional or always for Jersey?
250+
static class MetricsResourceConfigCustomizer {
251+
private final String path;
252+
253+
MetricsResourceConfigCustomizer(JerseyPathResolver pathResolver) {
254+
String path = pathResolver.getPath();
255+
if(path.endsWith("/*")){
256+
path = path.substring(0, path.lastIndexOf("/"));
257+
}
258+
this.path = path;
259+
}
260+
261+
@Bean
262+
public ResourceConfigCustomizer resourceConfigCustomizer() {
263+
return new ResourceConfigCustomizer() {
264+
@Override
265+
public void customize(ResourceConfig config) {
266+
config.register(pathTemplateRequestFilter());
267+
}
268+
};
269+
}
270+
271+
@Bean
272+
public ContainerRequestFilter pathTemplateRequestFilter() {
273+
return new ContainerRequestFilter() {
274+
@Override
275+
public void filter(ContainerRequestContext requestContext) {
276+
UriInfo uriInfo = requestContext.getUriInfo();
277+
if (uriInfo instanceof UriRoutingContext) {
278+
List<UriTemplate> templates = ((UriRoutingContext) uriInfo).getMatchedTemplates();
279+
if (templates.size() == 1) {
280+
//TODO static final somewhere
281+
requestContext.setProperty("JERSEY_TEMPLATE", path + templates.get(0).getTemplate());
282+
}
283+
}
284+
}
285+
};
286+
}
287+
}
288+
234289
@ConditionalOnClass(JacksonFeature.class)
235290
@ConditionalOnSingleCandidate(ObjectMapper.class)
236291
@Configuration

0 commit comments

Comments
 (0)