Commit a1b1cdb1 authored by Johannes Edmeier's avatar Johannes Edmeier Committed by Phillip Webb

Improve resource handling in LogFileMvcEndpoint

Update `LogFileMvcEndpoint` to use a `ResourceHttpRequestHandler` when
serving the log file resource. This gives support for requesting parts
of the logfile via the HTTP Range header. Requests with the
`If-Modified-Since` header are now also handled correctly.

Closes gh-4333
parent 77e88583
...@@ -18,6 +18,9 @@ package org.springframework.boot.actuate.endpoint.mvc; ...@@ -18,6 +18,9 @@ package org.springframework.boot.actuate.endpoint.mvc;
import java.io.IOException; 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.NotNull;
import javax.validation.constraints.Pattern; import javax.validation.constraints.Pattern;
...@@ -32,19 +35,18 @@ import org.springframework.context.EnvironmentAware; ...@@ -32,19 +35,18 @@ import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.ResponseEntity.BodyBuilder;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
/** /**
* Controller that provides an API for logfiles, i.e. downloading the main logfile * Controller that provides an API for logfiles, i.e. downloading the main logfile
* configured in environment property 'logging.file' that is standard, but optional * configured in environment property 'logging.file' that is standard, but optional
* property for spring-boot applications. * property for spring-boot applications.
* *
* @author Johannes Stelzer * @author Johannes Edmeier
* @author Phillip Webb * @author Phillip Webb
* @since 1.3.0 * @since 1.3.0
*/ */
...@@ -109,28 +111,15 @@ public class LogFileMvcEndpoint implements MvcEndpoint, EnvironmentAware { ...@@ -109,28 +111,15 @@ public class LogFileMvcEndpoint implements MvcEndpoint, EnvironmentAware {
return null; return null;
} }
@RequestMapping(method = RequestMethod.HEAD) @RequestMapping(method = { RequestMethod.GET, RequestMethod.HEAD })
@ResponseBody public void invoke(HttpServletRequest request, HttpServletResponse response)
public ResponseEntity<?> available() { throws ServletException, IOException {
return getResponse(false);
}
@RequestMapping(method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<?> invoke() throws IOException {
return getResponse(true);
}
private ResponseEntity<?> getResponse(boolean includeBody) {
if (!isEnabled()) { if (!isEnabled()) {
return (includeBody ? DISABLED_RESPONSE : ResponseEntity.notFound().build()); response.setStatus(HttpStatus.NOT_FOUND.value());
return;
} }
Resource resource = getLogFileResource(); Resource resource = getLogFileResource();
if (resource == null) { new Handler(resource).handleRequest(request, response);
return ResponseEntity.notFound().build();
}
BodyBuilder response = ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN);
return (includeBody ? response.body(resource) : response.build());
} }
private Resource getLogFileResource() { private Resource getLogFileResource() {
...@@ -149,4 +138,27 @@ public class LogFileMvcEndpoint implements MvcEndpoint, EnvironmentAware { ...@@ -149,4 +138,27 @@ public class LogFileMvcEndpoint implements MvcEndpoint, EnvironmentAware {
return resource; return resource;
} }
/**
* {@link ResourceHttpRequestHandler} to send the log file.
*/
private static class Handler extends ResourceHttpRequestHandler {
private final Resource resource;
public Handler(Resource resource) {
this.resource = resource;
}
@Override
protected Resource getResource(HttpServletRequest request) throws IOException {
return this.resource;
}
@Override
protected MediaType getMediaType(Resource resource) {
return MediaType.TEXT_PLAIN;
}
}
} }
...@@ -18,18 +18,17 @@ package org.springframework.boot.actuate.endpoint.mvc; ...@@ -18,18 +18,17 @@ package org.springframework.boot.actuate.endpoint.mvc;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.springframework.core.io.Resource; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.env.MockEnvironment;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
...@@ -39,7 +38,7 @@ import static org.junit.Assert.assertThat; ...@@ -39,7 +38,7 @@ import static org.junit.Assert.assertThat;
/** /**
* Tests for {@link LogFileMvcEndpoint}. * Tests for {@link LogFileMvcEndpoint}.
* *
* @author Johannes Stelzer * @author Johannes Edmeier
* @author Phillip Webb * @author Phillip Webb
*/ */
public class LogFileMvcEndpointTests { public class LogFileMvcEndpointTests {
...@@ -63,37 +62,54 @@ public class LogFileMvcEndpointTests { ...@@ -63,37 +62,54 @@ public class LogFileMvcEndpointTests {
} }
@Test @Test
public void notAvailableWithoutLogFile() throws IOException { public void notAvailableWithoutLogFile() throws Exception {
assertThat(this.mvc.available().getStatusCode(), equalTo(HttpStatus.NOT_FOUND)); MockHttpServletResponse response = new MockHttpServletResponse();
MockHttpServletRequest request = new MockHttpServletRequest(
HttpMethod.HEAD.name(), "/logfile");
this.mvc.invoke(request, response);
assertThat(response.getStatus(), equalTo(HttpStatus.NOT_FOUND.value()));
} }
@Test @Test
public void notAvailableWithMissingLogFile() throws Exception { public void notAvailableWithMissingLogFile() throws Exception {
this.environment.setProperty("logging.file", "no_test.log"); this.environment.setProperty("logging.file", "no_test.log");
assertThat(this.mvc.available().getStatusCode(), equalTo(HttpStatus.NOT_FOUND)); MockHttpServletResponse response = new MockHttpServletResponse();
MockHttpServletRequest request = new MockHttpServletRequest(
HttpMethod.HEAD.name(), "/logfile");
this.mvc.invoke(request, response);
assertThat(response.getStatus(), equalTo(HttpStatus.NOT_FOUND.value()));
} }
@Test @Test
public void availableWithLogFile() throws Exception { public void availableWithLogFile() throws Exception {
this.environment.setProperty("logging.file", this.logFile.getAbsolutePath()); this.environment.setProperty("logging.file", this.logFile.getAbsolutePath());
assertThat(this.mvc.available().getStatusCode(), equalTo(HttpStatus.OK)); MockHttpServletResponse response = new MockHttpServletResponse();
MockHttpServletRequest request = new MockHttpServletRequest(
HttpMethod.HEAD.name(), "/logfile");
this.mvc.invoke(request, response);
assertThat(response.getStatus(), equalTo(HttpStatus.OK.value()));
} }
@Test @Test
public void notAvailableIfDisabled() throws Exception { public void notAvailableIfDisabled() throws Exception {
this.environment.setProperty("logging.file", this.logFile.getAbsolutePath()); this.environment.setProperty("logging.file", this.logFile.getAbsolutePath());
this.mvc.setEnabled(false); this.mvc.setEnabled(false);
assertThat(this.mvc.available().getStatusCode(), equalTo(HttpStatus.NOT_FOUND)); MockHttpServletResponse response = new MockHttpServletResponse();
MockHttpServletRequest request = new MockHttpServletRequest(
HttpMethod.HEAD.name(), "/logfile");
this.mvc.invoke(request, response);
assertThat(response.getStatus(), equalTo(HttpStatus.NOT_FOUND.value()));
} }
@Test @Test
public void invokeGetsContent() throws IOException { public void invokeGetsContent() throws Exception {
this.environment.setProperty("logging.file", this.logFile.getAbsolutePath()); this.environment.setProperty("logging.file", this.logFile.getAbsolutePath());
ResponseEntity<?> response = this.mvc.invoke(); MockHttpServletResponse response = new MockHttpServletResponse();
assertEquals(HttpStatus.OK, response.getStatusCode()); MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(),
InputStream inputStream = ((Resource) response.getBody()).getInputStream(); "/logfile");
InputStreamReader reader = new InputStreamReader(inputStream); this.mvc.invoke(request, response);
assertEquals("--TEST--", FileCopyUtils.copyToString(reader)); assertThat(response.getStatus(), equalTo(HttpStatus.OK.value()));
assertEquals("--TEST--", response.getContentAsString());
} }
} }
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