From 8ca5635b69109fba898a094abd5970c906d29060 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 9 Apr 2015 18:10:51 -0700 Subject: [PATCH 1/2] Add regex support to /metrics and /env endpoints Update MetricsMvcEndpoint and EnvironmentMvcEndpoint to support regex filter of names. See gh-2252 Add it --- .../endpoint/mvc/EnvironmentMvcEndpoint.java | 53 +++++++++- .../endpoint/mvc/MetricsMvcEndpoint.java | 40 +++++++- .../endpoint/mvc/NamePatternFilter.java | 98 +++++++++++++++++++ .../mvc/EnvironmentMvcEndpointTests.java | 9 +- .../endpoint/mvc/NamePatternFilterTests.java | 78 +++++++++++++++ 5 files changed, 267 insertions(+), 11 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/NamePatternFilter.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/NamePatternFilterTests.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java index 3915131dedb..1aec58d6cd6 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java @@ -18,7 +18,11 @@ package org.springframework.boot.actuate.endpoint.mvc; import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySources; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -50,11 +54,7 @@ public class EnvironmentMvcEndpoint extends EndpointMvcAdapter implements // disabled return getDisabledResponse(); } - String result = this.environment.getProperty(name); - if (result == null) { - throw new NoSuchPropertyException("No such property: " + name); - } - return ((EnvironmentEndpoint) getDelegate()).sanitize(name, result); + return new NamePatternEnvironmentFilter(this.environment).getResults(name); } @Override @@ -62,6 +62,48 @@ public class EnvironmentMvcEndpoint extends EndpointMvcAdapter implements this.environment = environment; } + /** + * {@link NamePatternFilter} for the Environment source. + */ + private class NamePatternEnvironmentFilter extends NamePatternFilter { + + public NamePatternEnvironmentFilter(Environment source) { + super(source); + } + + @Override + protected void getNames(Environment source, NameCallback callback) { + if (source instanceof ConfigurableEnvironment) { + getNames(((ConfigurableEnvironment) source).getPropertySources(), + callback); + } + } + + private void getNames(PropertySources propertySources, NameCallback callback) { + for (PropertySource propertySource : propertySources) { + if (propertySource instanceof EnumerablePropertySource) { + EnumerablePropertySource source = (EnumerablePropertySource) propertySource; + for (String name : source.getPropertyNames()) { + callback.addName(name); + } + } + } + } + + @Override + protected Object getValue(Environment source, String name) { + String result = source.getProperty(name); + if (result == null) { + throw new NoSuchPropertyException("No such property: " + name); + } + return ((EnvironmentEndpoint) getDelegate()).sanitize(name, result); + } + + } + + /** + * Exception thrown when the specified property cannot be found. + */ @SuppressWarnings("serial") @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No such property") public static class NoSuchPropertyException extends RuntimeException { @@ -71,4 +113,5 @@ public class EnvironmentMvcEndpoint extends EndpointMvcAdapter implements } } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpoint.java index 3aeb7596469..5d1b4c054a7 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpoint.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.endpoint.mvc; +import java.util.Map; + import org.springframework.boot.actuate.endpoint.MetricsEndpoint; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PathVariable; @@ -29,6 +31,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; * * @author Dave Syer * @author Andy Wilkinson + * @author Sergei Egorov */ public class MetricsMvcEndpoint extends EndpointMvcAdapter { @@ -47,13 +50,39 @@ public class MetricsMvcEndpoint extends EndpointMvcAdapter { // disabled return getDisabledResponse(); } - Object value = this.delegate.invoke().get(name); - if (value == null) { - throw new NoSuchMetricException("No such metric: " + name); - } - return value; + return new NamePatternMapFilter(this.delegate.invoke()).getResults(name); } + /** + * {@link NamePatternFilter} for the Map source. + */ + private class NamePatternMapFilter extends NamePatternFilter> { + + public NamePatternMapFilter(Map source) { + super(source); + } + + @Override + protected void getNames(Map source, NameCallback callback) { + for (String name : source.keySet()) { + callback.addName(name); + } + } + + @Override + protected Object getValue(Map source, String name) { + Object value = source.get(name); + if (value == null) { + throw new NoSuchMetricException("No such metric: " + name); + } + return value; + } + + } + + /** + * Exception thrown when the specified metric cannot be found. + */ @SuppressWarnings("serial") @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No such metric") public static class NoSuchMetricException extends RuntimeException { @@ -63,4 +92,5 @@ public class MetricsMvcEndpoint extends EndpointMvcAdapter { } } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/NamePatternFilter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/NamePatternFilter.java new file mode 100644 index 00000000000..684a1e4637c --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/NamePatternFilter.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.mvc; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Utility class that can be used to filter source data using a name regular expression. + * Detects if the name is classic "single value" key or a regular expression. Subclasses + * must provide implementations of {@link #getValue(Object, String)} and + * {@link #getNames(Object, NameCallback)}. + * + * @author Phillip Webb + * @author Sergei Egorov + * @param The source data type + * @since 1.3.0 + */ +abstract class NamePatternFilter { + + private static final String[] REGEX_PARTS = { "*", "$", "^", "+" }; + + private final T source; + + public NamePatternFilter(T source) { + this.source = source; + } + + public Object getResults(String name) { + if (!isRegex(name)) { + return getValue(this.source, name); + } + Pattern pattern = Pattern.compile(name); + ResultCollectingNameCallback resultCollector = new ResultCollectingNameCallback( + pattern); + getNames(this.source, resultCollector); + return resultCollector.getResults(); + + } + + private boolean isRegex(String name) { + for (String part : REGEX_PARTS) { + if (name.contains(part)) { + return true; + } + } + return false; + } + + protected abstract void getNames(T source, NameCallback callback); + + protected abstract Object getValue(T source, String name); + + protected static interface NameCallback { + + void addName(String name); + + } + + private class ResultCollectingNameCallback implements NameCallback { + + private final Pattern pattern; + + private final Map results = new LinkedHashMap(); + + public ResultCollectingNameCallback(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public void addName(String name) { + if (this.pattern.matcher(name).matches()) { + this.results.put(name, getValue(NamePatternFilter.this.source, name)); + } + } + + public Map getResults() { + return this.results; + } + + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java index fa1f71dd163..5c778e35056 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java @@ -64,7 +64,7 @@ public class EnvironmentMvcEndpointTests { this.context.getBean(EnvironmentEndpoint.class).setEnabled(true); this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build(); EnvironmentTestUtils.addEnvironment( - (ConfigurableApplicationContext) this.context, "foo:bar"); + (ConfigurableApplicationContext) this.context, "foo:bar", "fool:baz"); } @Test @@ -85,6 +85,13 @@ public class EnvironmentMvcEndpointTests { this.mvc.perform(get("/env/foo")).andExpect(status().isNotFound()); } + @Test + public void regex() throws Exception { + this.mvc.perform(get("/env/foo.*")).andExpect(status().isOk()) + .andExpect(content().string(containsString("\"foo\":\"bar\""))) + .andExpect(content().string(containsString("\"fool\":\"baz\""))); + } + @Import({ EndpointWebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class }) @EnableWebMvc diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/NamePatternFilterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/NamePatternFilterTests.java new file mode 100644 index 00000000000..422196864e3 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/NamePatternFilterTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.mvc; + +import java.util.Map; + +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link NamePatternFilter}. + * + * @author Phillip Webb + */ +public class NamePatternFilterTests { + + @Test + public void nonRegex() throws Exception { + MockNamePatternFilter filter = new MockNamePatternFilter(); + assertThat(filter.getResults("not.a.regex"), equalTo((Object) "not.a.regex")); + assertThat(filter.isGetNamesCalled(), equalTo(false)); + } + + @Test + @SuppressWarnings("unchecked") + public void regex() throws Exception { + MockNamePatternFilter filter = new MockNamePatternFilter(); + Map results = (Map) filter.getResults("fo.*"); + assertThat(results.get("foo"), equalTo((Object) "foo")); + assertThat(results.get("fool"), equalTo((Object) "fool")); + assertThat(filter.isGetNamesCalled(), equalTo(true)); + + } + + private static class MockNamePatternFilter extends NamePatternFilter { + + public MockNamePatternFilter() { + super(null); + } + + private boolean getNamesCalled; + + @Override + protected Object getValue(Object source, String name) { + return name; + } + + @Override + protected void getNames(Object source, NameCallback callback) { + this.getNamesCalled = true; + callback.addName("foo"); + callback.addName("fool"); + callback.addName("fume"); + } + + public boolean isGetNamesCalled() { + return this.getNamesCalled; + } + + } + +} From a60df8184547bca021df08eb70ed4b61627c1491 Mon Sep 17 00:00:00 2001 From: Sergey Egorov Date: Thu, 9 Apr 2015 18:11:49 -0700 Subject: [PATCH 2/2] Add additional MetricsMvcEndpoint regex tests Update MetricsMvcEndpointTests to test for regular expression based calls. Closes gh-2252 --- .../endpoint/mvc/MetricsMvcEndpointTests.java | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java index fc50083c290..32fb1cacb40 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java @@ -16,8 +16,9 @@ package org.springframework.boot.actuate.endpoint.mvc; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import org.junit.Before; import org.junit.Test; @@ -50,6 +51,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * Tests for {@link MetricsMvcEndpoint} * * @author Andy Wilkinson + * @author Sergei Egorov */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = { TestConfiguration.class }) @@ -96,6 +98,34 @@ public class MetricsMvcEndpointTests { this.mvc.perform(get("/metrics/bar")).andExpect(status().isNotFound()); } + @Test + public void regexAll() throws Exception { + String expected = "{\"foo\":1,\"group1.a\":1,\"group1.b\":1,\"group2.a\":1,\"group2_a\":1}"; + this.mvc.perform(get("/metrics/.*")).andExpect(status().isOk()) + .andExpect(content().string(expected)); + } + + @Test + public void regexGroupDot() throws Exception { + String expected = "{\"group1.a\":1,\"group1.b\":1,\"group2.a\":1}"; + this.mvc.perform(get("/metrics/group[0-9]+\\..*")).andExpect(status().isOk()) + .andExpect(content().string(expected)); + } + + @Test + public void regexGroup1() throws Exception { + String expected = "{\"group1.a\":1,\"group1.b\":1}"; + this.mvc.perform(get("/metrics/group1\\..*")).andExpect(status().isOk()) + .andExpect(content().string(expected)); + } + + @Test + public void specificMetricWithDot() throws Exception { + this.mvc.perform(get("/metrics/group2.a")).andExpect(status().isOk()) + .andExpect(content().string("1")); + + } + @Import({ EndpointWebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class }) @EnableWebMvc @@ -108,7 +138,13 @@ public class MetricsMvcEndpointTests { @Override public Collection> metrics() { - return Arrays.> asList(new Metric("foo", 1)); + ArrayList> metrics = new ArrayList>(); + metrics.add(new Metric("foo", 1)); + metrics.add(new Metric("group1.a", 1)); + metrics.add(new Metric("group1.b", 1)); + metrics.add(new Metric("group2.a", 1)); + metrics.add(new Metric("group2_a", 1)); + return Collections.unmodifiableList(metrics); } });