From e1893f66cebcf4198aa800f2ae5c39b937d65ece Mon Sep 17 00:00:00 2001 From: Lari Hotari Date: Tue, 12 Apr 2016 21:42:29 -0400 Subject: [PATCH 1/3] Extract AbstractMvcEndpoint class Extract common functionality from Spring MVC only endpoints. See gh-5670 --- .../endpoint/mvc/AbstractMvcEndpoint.java | 113 ++++++++++++++++++ .../actuate/endpoint/mvc/DocsMvcEndpoint.java | 68 +---------- .../endpoint/mvc/HalJsonMvcEndpoint.java | 73 +---------- .../endpoint/mvc/JolokiaMvcEndpoint.java | 68 +---------- .../endpoint/mvc/LogFileMvcEndpoint.java | 66 +--------- 5 files changed, 129 insertions(+), 259 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractMvcEndpoint.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractMvcEndpoint.java new file mode 100644 index 0000000000..26ee8c2b7f --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractMvcEndpoint.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2016 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 javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.actuate.endpoint.EndpointProperties; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +/** + * Abstract base class for {@link MvcEndpoint} implementations without a backing + * {@link Endpoint}. + * + * @author Phillip Webb + * @author Lari Hotari + * @since 1.4.0 + */ +public abstract class AbstractMvcEndpoint extends WebMvcConfigurerAdapter + implements MvcEndpoint, EnvironmentAware { + + private Environment environment; + + /** + * Endpoint URL path. + */ + @NotNull + @Pattern(regexp = "/.*|^$", message = "Path must start with /") + private String path; + + /** + * Enable the endpoint. + */ + private Boolean enabled; + + /** + * Mark if the endpoint exposes sensitive information. + */ + private Boolean sensitive; + + private final boolean sensitiveDefault; + + public AbstractMvcEndpoint(String path, boolean sensitive) { + this.path = path; + this.sensitiveDefault = sensitive; + } + + public AbstractMvcEndpoint(String path, boolean sensitive, boolean enabled) { + this.path = path; + this.sensitiveDefault = sensitive; + this.enabled = enabled; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + protected final Environment getEnvironment() { + return this.environment; + } + + @Override + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + + public boolean isEnabled() { + return EndpointProperties.isEnabled(this.environment, this.enabled); + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + @Override + public boolean isSensitive() { + return EndpointProperties.isSensitive(this.environment, this.sensitive, + this.sensitiveDefault); + } + + public void setSensitive(Boolean sensitive) { + this.sensitive = sensitive; + } + + @Override + @SuppressWarnings("rawtypes") + public Class getEndpointType() { + return null; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/DocsMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/DocsMvcEndpoint.java index 3873ff1b52..06b009d1cc 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/DocsMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/DocsMvcEndpoint.java @@ -16,15 +16,10 @@ package org.springframework.boot.actuate.endpoint.mvc; -import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.actuate.endpoint.EndpointProperties; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.Environment; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; /** * {@link MvcEndpoint} to expose actuator documentation. @@ -33,95 +28,42 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter * @since 1.3.0 */ @ConfigurationProperties("endpoints.docs") -public class DocsMvcEndpoint extends WebMvcConfigurerAdapter - implements MvcEndpoint, EnvironmentAware { +public class DocsMvcEndpoint extends AbstractMvcEndpoint { private static final String DOCS_LOCATION = "classpath:/META-INF/resources/spring-boot-actuator/docs/"; - private Environment environment; - - /** - * Endpoint URL path. - */ - private String path = "/docs"; - - /** - * Enable the endpoint. - */ - private boolean enabled = true; - - /** - * Mark if the endpoint exposes sensitive information. - */ - private Boolean sensitive; - private final ManagementServletContext managementServletContext; private Curies curies = new Curies(); - @Override - public void setEnvironment(Environment environment) { - this.environment = environment; - } - public Curies getCuries() { return this.curies; } public DocsMvcEndpoint(ManagementServletContext managementServletContext) { + super("/docs", false); this.managementServletContext = managementServletContext; } @RequestMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE) public String browse() { - return "forward:" + this.managementServletContext.getContextPath() + this.path + return "forward:" + this.managementServletContext.getContextPath() + getPath() + "/index.html"; } @RequestMapping(value = "", produces = MediaType.TEXT_HTML_VALUE) public String redirect() { - return "redirect:" + this.managementServletContext.getContextPath() + this.path + return "redirect:" + this.managementServletContext.getContextPath() + getPath() + "/"; } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler( - this.managementServletContext.getContextPath() + this.path + "/**") + this.managementServletContext.getContextPath() + getPath() + "/**") .addResourceLocations(DOCS_LOCATION); } - public void setPath(String path) { - this.path = path; - } - - @Override - public String getPath() { - return this.path; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - @Override - public boolean isSensitive() { - return EndpointProperties.isSensitive(this.environment, this.sensitive, false); - } - - public void setSensitive(Boolean sensitive) { - this.sensitive = sensitive; - } - - @Override - public Class> getEndpointType() { - return null; - } - /** * Properties of the default CurieProvider (used for adding docs links). If enabled, * all unqualified rels will pick up a prefix and a curie template pointing to the diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalJsonMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalJsonMvcEndpoint.java index 8f36f366db..5f9d3afa82 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalJsonMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalJsonMvcEndpoint.java @@ -16,20 +16,12 @@ package org.springframework.boot.actuate.endpoint.mvc; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Pattern; - -import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.actuate.endpoint.EndpointProperties; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.Environment; import org.springframework.hateoas.ResourceSupport; import org.springframework.http.MediaType; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; /** * {@link MvcEndpoint} to expose HAL-formatted JSON. @@ -40,41 +32,16 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter * @since 1.3.0 */ @ConfigurationProperties("endpoints.actuator") -public class HalJsonMvcEndpoint extends WebMvcConfigurerAdapter - implements MvcEndpoint, EnvironmentAware { - - private Environment environment; - - /** - * Endpoint URL path. - */ - @NotNull - @Pattern(regexp = "^$|/.*", message = "Path must be empty or start with /") - private String path; - - /** - * Enable the endpoint. - */ - private boolean enabled = true; - - /** - * Mark if the endpoint exposes sensitive information. - */ - private Boolean sensitive; - +public class HalJsonMvcEndpoint extends AbstractMvcEndpoint { private final ManagementServletContext managementServletContext; public HalJsonMvcEndpoint(ManagementServletContext managementServletContext) { + super(getDefaultPath(managementServletContext), false); this.managementServletContext = managementServletContext; - this.path = getDefaultPath(managementServletContext); } - @Override - public void setEnvironment(Environment environment) { - this.environment = environment; - } - - private String getDefaultPath(ManagementServletContext managementServletContext) { + private static String getDefaultPath( + ManagementServletContext managementServletContext) { if (StringUtils.hasText(managementServletContext.getContextPath())) { return ""; } @@ -87,39 +54,7 @@ public class HalJsonMvcEndpoint extends WebMvcConfigurerAdapter return new ResourceSupport(); } - public void setPath(String path) { - this.path = path; - } - - @Override - public String getPath() { - return this.path; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - @Override - public boolean isSensitive() { - return EndpointProperties.isSensitive(this.environment, this.sensitive, false); - } - - public void setSensitive(Boolean sensitive) { - this.sensitive = sensitive; - } - - @Override - public Class> getEndpointType() { - return null; - } - protected final ManagementServletContext getManagementServletContext() { return this.managementServletContext; } - } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpoint.java index d2879d6628..0ab3dd7b59 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpoint.java @@ -22,20 +22,14 @@ import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Pattern; import org.jolokia.http.AgentServlet; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; -import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.actuate.endpoint.EndpointProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.Environment; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.ServletContextAware; import org.springframework.web.servlet.ModelAndView; @@ -50,31 +44,12 @@ import org.springframework.web.util.UrlPathHelper; */ @ConfigurationProperties(prefix = "endpoints.jolokia", ignoreUnknownFields = false) @HypermediaDisabled -public class JolokiaMvcEndpoint implements MvcEndpoint, InitializingBean, - ApplicationContextAware, ServletContextAware, EnvironmentAware { - - private Environment environment; - - /** - * Endpoint URL path. - */ - @NotNull - @Pattern(regexp = "/.*", message = "Path must start with /") - private String path = "/jolokia"; - - /** - * Enable the endpoint. - */ - private boolean enabled = true; - - /** - * Mark if the endpoint exposes sensitive information. - */ - private Boolean sensitive; - +public class JolokiaMvcEndpoint extends AbstractMvcEndpoint + implements InitializingBean, ApplicationContextAware, ServletContextAware { private final ServletWrappingController controller = new ServletWrappingController(); public JolokiaMvcEndpoint() { + super("/jolokia", true); this.controller.setServletClass(AgentServlet.class); this.controller.setServletName("jolokia"); } @@ -99,43 +74,6 @@ public class JolokiaMvcEndpoint implements MvcEndpoint, InitializingBean, this.controller.setApplicationContext(context); } - @Override - public void setEnvironment(Environment environment) { - this.environment = environment; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - @Override - public boolean isSensitive() { - return EndpointProperties.isSensitive(this.environment, this.sensitive, true); - } - - public void setSensitive(Boolean sensitive) { - this.sensitive = sensitive; - } - - @Override - public String getPath() { - return this.path; - } - - public void setPath(String path) { - this.path = path; - } - - @Override - @SuppressWarnings("rawtypes") - public Class getEndpointType() { - return null; - } - @RequestMapping("/**") public ModelAndView handle(HttpServletRequest request, HttpServletResponse response) throws Exception { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LogFileMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LogFileMvcEndpoint.java index 338d2a8d85..f41b6bebbf 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LogFileMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LogFileMvcEndpoint.java @@ -22,18 +22,12 @@ import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.actuate.endpoint.EndpointProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.logging.LogFile; -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.Environment; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; @@ -52,64 +46,18 @@ import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; * @since 1.3.0 */ @ConfigurationProperties(prefix = "endpoints.logfile") -public class LogFileMvcEndpoint implements MvcEndpoint, EnvironmentAware { +public class LogFileMvcEndpoint extends AbstractMvcEndpoint { private static final Log logger = LogFactory.getLog(LogFileMvcEndpoint.class); - /** - * Endpoint URL path. - */ - @NotNull - @Pattern(regexp = "/.*", message = "Path must start with /") - private String path = "/logfile"; - - /** - * Enable the endpoint. - */ - private boolean enabled = true; - - /** - * Mark if the endpoint exposes sensitive information. - */ - private Boolean sensitive; - /** * External Logfile to be accessed. Can be used if the logfile is written by output * redirect and not by the logging-system itself. */ private File externalFile; - private Environment environment; - - @Override - public void setEnvironment(Environment environment) { - this.environment = environment; - } - - @Override - public String getPath() { - return this.path; - } - - public void setPath(String path) { - this.path = path; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - @Override - public boolean isSensitive() { - return EndpointProperties.isSensitive(this.environment, this.sensitive, true); - } - - public void setSensitive(Boolean sensitive) { - this.sensitive = sensitive; + public LogFileMvcEndpoint() { + super("/logfile", true); } public File getExternalFile() { @@ -120,12 +68,6 @@ public class LogFileMvcEndpoint implements MvcEndpoint, EnvironmentAware { this.externalFile = externalFile; } - @Override - @SuppressWarnings("rawtypes") - public Class getEndpointType() { - return null; - } - @RequestMapping(method = { RequestMethod.GET, RequestMethod.HEAD }) public void invoke(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { @@ -147,7 +89,7 @@ public class LogFileMvcEndpoint implements MvcEndpoint, EnvironmentAware { if (this.externalFile != null) { return new FileSystemResource(this.externalFile); } - LogFile logFile = LogFile.get(this.environment); + LogFile logFile = LogFile.get(getEnvironment()); if (logFile == null) { logger.debug("Missing 'logging.file' or 'logging.path' properties"); return null; From 9f07e944500df6a86b32ed8cec1f5486cb0c8680 Mon Sep 17 00:00:00 2001 From: Lari Hotari Date: Tue, 12 Apr 2016 21:42:42 -0400 Subject: [PATCH 2/3] Add MVC actuator endpoint for heap dumps Add MVC only endpoint to obtain GZip compressed heap dump files. See gh-5670 --- .../hypermedia/EndpointDocumentation.java | 17 +- ...tWebMvcManagementContextConfiguration.java | 8 + .../endpoint/mvc/HeapdumpMvcEndpoint.java | 233 ++++++++++++++++++ .../EndpointWebMvcAutoConfigurationTests.java | 4 +- ...erMvcEndpointDisabledIntegrationTests.java | 2 +- ...serMvcEndpointVanillaIntegrationTests.java | 2 +- .../mvc/HeapdumpMvcEndpointTests.java | 171 +++++++++++++ 7 files changed, 427 insertions(+), 10 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpoint.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointTests.java diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java index 84213de856..338b234165 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java @@ -20,12 +20,15 @@ import java.io.File; import java.io.FileOutputStream; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import groovy.text.Template; import groovy.text.TemplateEngine; @@ -70,6 +73,9 @@ public class EndpointDocumentation { static final File LOG_FILE = new File("target/logs/spring.log"); + private static final Set SKIPPED = Collections.unmodifiableSet( + new HashSet(Arrays.asList("/docs", "/logfile", "/heapdump"))); + @Autowired private MvcEndpoints mvcEndpoints; @@ -103,28 +109,27 @@ public class EndpointDocumentation { @Test public void endpoints() throws Exception { - final File docs = new File("src/main/asciidoc"); - final Map model = new LinkedHashMap(); final List endpoints = new ArrayList(); model.put("endpoints", endpoints); for (MvcEndpoint endpoint : getEndpoints()) { - final String endpointPath = StringUtils.hasText(endpoint.getPath()) - ? endpoint.getPath() : "/"; - - if (!endpointPath.equals("/docs") && !endpointPath.equals("/logfile")) { + final String endpointPath = (StringUtils.hasText(endpoint.getPath()) + ? endpoint.getPath() : "/"); + if (!SKIPPED.contains(endpointPath)) { String output = endpointPath.substring(1); output = output.length() > 0 ? output : "./"; this.mockMvc.perform(get(endpointPath).accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document(output)) .andDo(new ResultHandler() { + @Override public void handle(MvcResult mvcResult) throws Exception { EndpointDoc endpoint = new EndpointDoc(docs, endpointPath); endpoints.add(endpoint); } + }); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java index b3ac138bd1..b12cd3f8de 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java @@ -31,6 +31,7 @@ import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCustomizer; import org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.HeapdumpMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.LogFileMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; @@ -132,6 +133,13 @@ public class EndpointWebMvcManagementContextConfiguration { return new EnvironmentMvcEndpoint(delegate); } + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint("heapdump") + public HeapdumpMvcEndpoint heapdumpMvcEndpoint() { + return new HeapdumpMvcEndpoint(); + } + @Bean @ConditionalOnBean(HealthEndpoint.class) @ConditionalOnEnabledEndpoint("health") diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpoint.java new file mode 100644 index 0000000000..4b631d8485 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpoint.java @@ -0,0 +1,233 @@ +/* + * Copyright 2012-2016 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.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.management.ManagementFactory; +import java.lang.management.PlatformManagedObject; +import java.lang.reflect.Method; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.zip.GZIPOutputStream; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.lang.UsesJava7; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * {@link MvcEndpoint} to expose heap dumps. + * + * @author Lari Hotari + * @author Phillip Webb + * @since 1.4.0 + */ +@ConfigurationProperties("endpoints.heapdump") +@HypermediaDisabled +public class HeapdumpMvcEndpoint extends AbstractMvcEndpoint implements MvcEndpoint { + + private final long timeout; + + private final Lock lock = new ReentrantLock(); + + private HeapDumper heapDumper; + + public HeapdumpMvcEndpoint() { + this(TimeUnit.SECONDS.toMillis(10)); + } + + protected HeapdumpMvcEndpoint(long timeout) { + super("/heapdump", true); + this.timeout = timeout; + } + + @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public void invoke(@RequestParam(defaultValue = "true") boolean live, + HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + if (!isEnabled()) { + response.setStatus(HttpStatus.NOT_FOUND.value()); + return; + } + try { + if (this.lock.tryLock(this.timeout, TimeUnit.MILLISECONDS)) { + try { + dumpHeap(live, request, response); + return; + } + finally { + this.lock.unlock(); + } + } + } + catch (InterruptedException ex) { + // Ignore + } + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + } + + private void dumpHeap(boolean live, HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException, InterruptedException { + if (this.heapDumper == null) { + this.heapDumper = createHeapDumper(); + } + File file = createTempFile(live); + try { + this.heapDumper.dumpHeap(file, live); + handle(file, request, response); + } + finally { + file.delete(); + } + } + + private File createTempFile(boolean live) throws IOException { + String date = new SimpleDateFormat("yyyy-MM-dd-HH-mm").format(new Date()); + File file = File.createTempFile("heapdump" + date + (live ? "-live" : ""), + ".hprof"); + file.delete(); + return file; + } + + /** + * Factory method used to create the {@link HeapDumper}. + * @return the heap dumper to use + * @throws HeapDumperUnavailableException if the heap dumper cannot be created + */ + protected HeapDumper createHeapDumper() throws HeapDumperUnavailableException { + return new HotSpotDiagnosticMXBeanHeapDumper(); + } + + /** + * Handle the heap dump file and respond. By default this method will return the + * response as a GZip stream. + * @param heapDumpFile the generated dump file + * @param request the HTTP request + * @param response the HTTP response + * @throws ServletException on servlet error + * @throws IOException on IO error + */ + protected void handle(File heapDumpFile, HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", + "attachment; filename=\"" + (heapDumpFile.getName() + ".gz") + "\""); + try { + InputStream in = new FileInputStream(heapDumpFile); + try { + GZIPOutputStream out = new GZIPOutputStream(response.getOutputStream()); + StreamUtils.copy(in, out); + out.finish(); + } + catch (NullPointerException ex) { + } + finally { + try { + in.close(); + } + catch (Throwable ex) { + } + } + } + catch (FileNotFoundException ex) { + } + } + + /** + * Strategy interface used to dump the heap to a file. + */ + protected interface HeapDumper { + + /** + * Dump the current heap to the specified file. + * @param file the file to dump the heap to + * @param live if only live objects (i.e. objects that are reachable from + * others) should be dumped + * @throws IOException on IO error + * @throws InterruptedException on thread interruption + */ + void dumpHeap(File file, boolean live) throws IOException, InterruptedException; + + } + + /** + * {@link HeapDumper} that uses {@code com.sun.management.HotSpotDiagnosticMXBean} + * available on Oracle and OpenJDK to dump the heap to a file. + */ + @UsesJava7 + protected static class HotSpotDiagnosticMXBeanHeapDumper implements HeapDumper { + + private Object diagnosticMXBean; + + private Method dumpHeapMethod; + + @SuppressWarnings("unchecked") + protected HotSpotDiagnosticMXBeanHeapDumper() { + try { + Class diagnosticMXBeanClass = ClassUtils.resolveClassName( + "com.sun.management.HotSpotDiagnosticMXBean", null); + this.diagnosticMXBean = ManagementFactory.getPlatformMXBean( + (Class) diagnosticMXBeanClass); + this.dumpHeapMethod = ReflectionUtils.findMethod(diagnosticMXBeanClass, + "dumpHeap", String.class, Boolean.TYPE); + } + catch (Throwable ex) { + throw new HeapDumperUnavailableException( + "Unable to locate HotSpotDiagnosticMXBean", ex); + } + } + + @Override + public void dumpHeap(File file, boolean live) { + ReflectionUtils.invokeMethod(this.dumpHeapMethod, this.diagnosticMXBean, + file.getAbsolutePath(), live); + } + + } + + /** + * Exception to be thrown if the {@link HeapDumper} cannot be created. + */ + @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) + protected static class HeapDumperUnavailableException extends RuntimeException { + + public HeapDumperUnavailableException(String message, Throwable cause) { + super(message, cause); + } + + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java index f44386c11e..4ce12702fc 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java @@ -429,8 +429,8 @@ public class EndpointWebMvcAutoConfigurationTests { this.applicationContext.register(RootConfig.class, BaseConfiguration.class, ServerPortConfig.class, EndpointWebMvcAutoConfiguration.class); this.applicationContext.refresh(); - // /health, /metrics, /env, /actuator (/shutdown is disabled by default) - assertThat(this.applicationContext.getBeansOfType(MvcEndpoint.class)).hasSize(4); + // /health, /metrics, /env, /actuator, /heapdump (/shutdown is disabled by default) + assertThat(this.applicationContext.getBeansOfType(MvcEndpoint.class)).hasSize(5); } @Test diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java index 0188770198..ab6683b671 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java @@ -83,7 +83,7 @@ public class HalBrowserMvcEndpointDisabledIntegrationTests { public void endpointsDoNotHaveLinks() throws Exception { for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) { String path = endpoint.getPath(); - if ("/actuator".equals(path)) { + if ("/actuator".equals(path) || endpoint instanceof HeapdumpMvcEndpoint) { continue; } path = path.length() > 0 ? path : "/"; diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java index 184acd81a0..209170a139 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java @@ -122,7 +122,7 @@ public class HalBrowserMvcEndpointVanillaIntegrationTests { @Test public void endpointsEachHaveSelf() throws Exception { Set collections = new HashSet( - Arrays.asList("/trace", "/beans", "/dump")); + Arrays.asList("/trace", "/beans", "/dump", "/heapdump")); for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) { String path = endpoint.getPath(); if (collections.contains(path)) { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointTests.java new file mode 100644 index 0000000000..b8ed37f3e8 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointTests.java @@ -0,0 +1,171 @@ +/* + * Copyright 2012-2016 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.io.File; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; + +import org.fusesource.hawtbuf.ByteArrayInputStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.context.WebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link HeapdumpMvcEndpoint}. + * + * @author Phillip Webb + */ +@RunWith(SpringRunner.class) +@SpringBootTest +public class HeapdumpMvcEndpointTests { + + @Autowired + private WebApplicationContext context; + + private MockMvc mvc; + + @Autowired + private TestHeapdumpMvcEndpoint endpoint; + + @Before + public void setup() { + this.context.getBean(HeapdumpMvcEndpoint.class).setEnabled(true); + this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build(); + } + + @After + public void reset() { + this.endpoint.reset(); + } + + @Test + public void invokeWhenDisabledShouldReturnNotFoundStatus() throws Exception { + this.endpoint.setEnabled(false); + this.mvc.perform(get("/heapdump")).andExpect(status().isNotFound()); + } + + @Test + public void invokeWhenNotAvailableShouldReturnServiceUnavailableStatus() + throws Exception { + this.endpoint.setAvailable(false); + this.mvc.perform(get("/heapdump")).andExpect(status().isServiceUnavailable()); + } + + @Test + public void invokeWhenLockedShouldReturnTooManyRequestsStatus() throws Exception { + this.endpoint.setLocked(true); + this.mvc.perform(get("/heapdump")).andExpect(status().isTooManyRequests()); + } + + @Test + public void invokeShouldReturnGzipContent() throws Exception { + MvcResult result = this.mvc.perform(get("/heapdump")).andExpect(status().isOk()) + .andReturn(); + byte[] bytes = result.getResponse().getContentAsByteArray(); + GZIPInputStream stream = new GZIPInputStream(new ByteArrayInputStream(bytes)); + byte[] uncompressed = FileCopyUtils.copyToByteArray(stream); + assertThat(uncompressed).isEqualTo("HEAPDUMP".getBytes()); + } + + @Import({ JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, WebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class }) + @Configuration + public static class TestConfiguration { + + @Bean + public HeapdumpMvcEndpoint endpoint() { + return new TestHeapdumpMvcEndpoint(); + } + + } + + private static class TestHeapdumpMvcEndpoint extends HeapdumpMvcEndpoint { + + private boolean available; + + private boolean locked; + + private String heapDump; + + TestHeapdumpMvcEndpoint() { + super(TimeUnit.SECONDS.toMillis(1)); + reset(); + } + + public void reset() { + this.available = true; + this.locked = false; + this.heapDump = "HEAPDUMP"; + } + + @Override + protected HeapDumper createHeapDumper() { + return new HeapDumper() { + + @Override + public void dumpHeap(File file, boolean live) + throws IOException, InterruptedException { + if (!TestHeapdumpMvcEndpoint.this.available) { + throw new HeapDumperUnavailableException("Not available", null); + } + if (TestHeapdumpMvcEndpoint.this.locked) { + throw new InterruptedException(); + } + FileCopyUtils.copy(TestHeapdumpMvcEndpoint.this.heapDump.getBytes(), + file); + } + + }; + } + + public void setAvailable(boolean available) { + this.available = available; + } + + public void setLocked(boolean locked) { + this.locked = locked; + } + + } + +} From 2583a534e6fdc9b407544f077cd1a3f7de059be8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 2 Jul 2016 11:07:09 -0700 Subject: [PATCH 3/3] Document heapdump endpoint See gh-5670 --- .../asciidoc/production-ready-features.adoc | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index e7704882f5..22bab09e05 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -62,7 +62,7 @@ The way that endpoints are exposed will depend on the type of technology that yo Most applications choose HTTP monitoring, where the ID of the endpoint is mapped to a URL. For example, by default, the `health` endpoint will be mapped to `/health`. -The following endpoints are available: +The following technology agnostic endpoints are available: [cols="2,5,1"] |=== @@ -86,11 +86,6 @@ HATEOAS to be on the classpath. |Displays a collated list of all `@ConfigurationProperties`. |true -|`docs` -|Displays documentation, including example requests and responses, for the Actuator's -endpoints. Requires `spring-boot-actuator-docs` to be on the classpath. -|false - |`dump` |Performs a thread dump. |true @@ -104,8 +99,9 @@ endpoints. Requires `spring-boot-actuator-docs` to be on the classpath. |true |`health` -|Shows application health information (when the application is secure, a simple '`status`' when accessed over an -unauthenticated connection or full message details when authenticated). +|Shows application health information (when the application is secure, a simple '`status`' +when accessed over an unauthenticated connection or full message details when +authenticated). |false |`info` @@ -116,12 +112,6 @@ unauthenticated connection or full message details when authenticated). |Shows any Liquibase database migrations that have been applied. |true -|`logfile` -|Returns the contents of the logfile (if `logging.file` or `logging.path` properties have -been set). Only available via MVC. Supports the use of the HTTP `Range` header to retrieve -part of the log file's content. -|true - |`metrics` |Shows '`metrics`' information for the current application. |true @@ -139,6 +129,32 @@ part of the log file's content. |true |=== +If you are using Spring MVC, the following additional endpoints can also be used: + +[cols="2,5,1"] +|=== +| ID | Description | Sensitive Default + +|`docs` +|Displays documentation, including example requests and responses, for the Actuator's +endpoints. Requires `spring-boot-actuator-docs` to be on the classpath. +|false + +|`heapdump` +|Returns a GZip compressed `hprof` heap dump file. +|true + +|`jolokia` +|Exposes JMX beans over HTTP (when Jolokia is on the classpath). +|true + +|`logfile` +|Returns the contents of the logfile (if `logging.file` or `logging.path` properties have +been set). Supports the use of the HTTP `Range` header to retrieve part of the log file's +content. +|true +|=== + NOTE: Depending on how an endpoint is exposed, the `sensitive` property may be used as a security hint. For example, sensitive endpoints will require a username/password when they are accessed over HTTP (or simply disabled if web security is not enabled).