Commit 9f07e944 authored by Lari Hotari's avatar Lari Hotari Committed by Phillip Webb

Add MVC actuator endpoint for heap dumps

Add MVC only endpoint to obtain GZip compressed heap dump files.

See gh-5670
parent e1893f66
......@@ -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<String> SKIPPED = Collections.<String>unmodifiableSet(
new HashSet<String>(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<String, Object> model = new LinkedHashMap<String, Object>();
final List<EndpointDoc> endpoints = new ArrayList<EndpointDoc>();
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);
}
});
}
}
......
......@@ -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")
......
/*
* 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 <em>live</em> 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<PlatformManagedObject>) 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);
}
}
}
......@@ -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
......
......@@ -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 : "/";
......
......@@ -122,7 +122,7 @@ public class HalBrowserMvcEndpointVanillaIntegrationTests {
@Test
public void endpointsEachHaveSelf() throws Exception {
Set<String> collections = new HashSet<String>(
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)) {
......
/*
* 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;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment