From 308a5eaff5d3d2ee53b18d456dea7b3d36429bc6 Mon Sep 17 00:00:00 2001 From: Johannes Stelzer Date: Tue, 6 Jan 2015 20:10:48 +0100 Subject: [PATCH] Add /logfile MVC actuator endpoint Add a `/logfile` endpoint which can be used to fetch the contents of the log file (if one is being used). Fixes gh-2137 Closes gh-2294 --- .../EndpointWebMvcAutoConfiguration.java | 8 + .../endpoint/mvc/EndpointMvcAdapter.java | 15 +- .../endpoint/mvc/LogFileMvcEndpoint.java | 152 ++++++++++++++++++ .../actuate/endpoint/mvc/MvcEndpoint.java | 9 ++ .../EndpointWebMvcAutoConfigurationTests.java | 2 +- .../endpoint/mvc/JolokiaMvcEndpointTests.java | 10 +- .../endpoint/mvc/LogFileMvcEndpointTests.java | 104 ++++++++++++ .../appendix-application-properties.adoc | 3 + .../asciidoc/production-ready-features.adoc | 5 + 9 files changed, 291 insertions(+), 17 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LogFileMvcEndpoint.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LogFileMvcEndpointTests.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java index af83416297..5f3fae9a42 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java @@ -45,6 +45,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.LogFileMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; @@ -90,6 +91,7 @@ import org.springframework.web.servlet.DispatcherServlet; * @author Phillip Webb * @author Christian Dupuis * @author Andy Wilkinson + * @author Johannes Stelzer */ @Configuration @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) @@ -219,6 +221,12 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, return new MetricsMvcEndpoint(delegate); } + @Bean + @ConditionalOnEnabledEndpoint("logfile") + public LogFileMvcEndpoint logfileMvcEndpoint() { + return new LogFileMvcEndpoint(); + } + @Bean @ConditionalOnBean(ShutdownEndpoint.class) @ConditionalOnEnabledEndpoint(value = "shutdown", enabledByDefault = false) diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointMvcAdapter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointMvcAdapter.java index e6159326a2..aa152f5854 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointMvcAdapter.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointMvcAdapter.java @@ -16,11 +16,7 @@ package org.springframework.boot.actuate.endpoint.mvc; -import java.util.Collections; -import java.util.Map; - import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestMapping; @@ -35,10 +31,6 @@ import org.springframework.web.bind.annotation.ResponseBody; */ public class EndpointMvcAdapter implements MvcEndpoint { - private final ResponseEntity> disabledResponse = new ResponseEntity>( - Collections.singletonMap("message", "This endpoint is disabled"), - HttpStatus.NOT_FOUND); - private final Endpoint delegate; /** @@ -54,9 +46,8 @@ public class EndpointMvcAdapter implements MvcEndpoint { @ResponseBody public Object invoke() { if (!this.delegate.isEnabled()) { - // Shouldn't happen - MVC endpoint shouldn't be registered when delegate's - // disabled - return this.disabledResponse; + // Shouldn't happen - shouldn't be registered when delegate's disabled + return getDisabledResponse(); } return this.delegate.invoke(); } @@ -89,7 +80,7 @@ public class EndpointMvcAdapter implements MvcEndpoint { * @return The response to be returned when the endpoint is disabled */ protected ResponseEntity getDisabledResponse() { - return this.disabledResponse; + return MvcEndpoint.DISABLED_RESPONSE; } } 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 new file mode 100644 index 0000000000..684b6b2d53 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LogFileMvcEndpoint.java @@ -0,0 +1,152 @@ +/* + * 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.io.IOException; + +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.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.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.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * Controller that provides an API for logfiles, i.e. downloading the main logfile + * configured in environment property 'logging.file' that is standard, but optional + * property for spring-boot applications. + * + * @author Johannes Stelzer + * @author Phillip Webb + * @since 1.3.0 + */ +@ConfigurationProperties(prefix = "endpoints.logfile") +public class LogFileMvcEndpoint implements MvcEndpoint, EnvironmentAware { + + 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 security on the endpoint. + */ + + private boolean sensitive = true; + + /** + * Enable the endpoint. + */ + + private boolean enabled = true; + + 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; + } + + @Override + public boolean isSensitive() { + return this.sensitive; + } + + public void setSensitive(boolean sensitive) { + this.sensitive = sensitive; + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + @SuppressWarnings("rawtypes") + public Class getEndpointType() { + return null; + } + + @RequestMapping(method = RequestMethod.HEAD) + @ResponseBody + public ResponseEntity available() { + return getResponse(false); + } + + @RequestMapping(method = RequestMethod.GET) + @ResponseBody + public ResponseEntity invoke() throws IOException { + return getResponse(true); + } + + private ResponseEntity getResponse(boolean includeBody) { + if (!isEnabled()) { + return (includeBody ? DISABLED_RESPONSE : ResponseEntity.notFound().build()); + } + Resource resource = getLogFileResource(); + if (resource == null) { + return ResponseEntity.notFound().build(); + } + BodyBuilder response = ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN); + return (includeBody ? response.body(resource) : response.build()); + } + + private Resource getLogFileResource() { + LogFile logFile = LogFile.get(this.environment); + if (logFile == null) { + logger.debug("Missing 'logging.file' or 'logging.path' properties"); + return null; + } + FileSystemResource resource = new FileSystemResource(logFile.toString()); + if (!resource.exists()) { + if (logger.isWarnEnabled()) { + logger.debug("Log file '" + resource + "' does not exist"); + } + return null; + } + return resource; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoint.java index c7cb66a83e..b5d74d2b6a 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoint.java @@ -16,7 +16,12 @@ package org.springframework.boot.actuate.endpoint.mvc; +import java.util.Collections; +import java.util.Map; + import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; /** * A strategy for the MVC layer on top of an {@link Endpoint}. Implementations are allowed @@ -29,6 +34,10 @@ import org.springframework.boot.actuate.endpoint.Endpoint; */ public interface MvcEndpoint { + public static final ResponseEntity> DISABLED_RESPONSE = new ResponseEntity>( + Collections.singletonMap("message", "This endpoint is disabled"), + HttpStatus.NOT_FOUND); + /** * Return the MVC path of the endpoint. * @return the endpoint path 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 de1aea38e9..7d32ffac58 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 @@ -294,7 +294,7 @@ public class EndpointWebMvcAutoConfigurationTests { this.applicationContext.refresh(); // /health, /metrics, /env (/shutdown is disabled by default) assertThat(this.applicationContext.getBeansOfType(MvcEndpoint.class).size(), - is(equalTo(3))); + is(equalTo(4))); } @Test diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointTests.java index 9b02bb663c..b41647183f 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.endpoint.mvc; import java.util.Set; +import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,8 +41,9 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -73,10 +75,10 @@ public class JolokiaMvcEndpointTests { } @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) public void endpointRegistered() throws Exception { Set values = this.endpoints.getEndpoints(); - assertEquals(1, values.size()); - assertTrue(values.iterator().next() instanceof JolokiaMvcEndpoint); + assertThat(values, (Matcher) hasItem(instanceOf(JolokiaMvcEndpoint.class))); } @Test diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LogFileMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LogFileMvcEndpointTests.java new file mode 100644 index 0000000000..50055074ee --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LogFileMvcEndpointTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2013-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.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.FileCopyUtils; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link LogFileMvcEndpoint}. + * + * @author Johannes Stelzer + * @author Phillip Webb + */ +public class LogFileMvcEndpointTests { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private LogFileMvcEndpoint mvc; + + private MockEnvironment environment; + + private File logFile; + + @Before + public void before() throws IOException { + this.logFile = this.temp.newFile(); + FileCopyUtils.copy("--TEST--".getBytes(), this.logFile); + this.environment = new MockEnvironment(); + this.mvc = new LogFileMvcEndpoint(); + this.mvc.setEnvironment(this.environment); + } + + @After + public void after() { + new File("test.log").delete(); + } + + @Test + public void notAvailableWithoutLogFile() throws IOException { + assertThat(this.mvc.available().getStatusCode(), equalTo(HttpStatus.NOT_FOUND)); + } + + @Test + public void notAvailableWithMissingLogFile() throws Exception { + this.environment.setProperty("logging.file", "no_test.log"); + assertThat(this.mvc.available().getStatusCode(), equalTo(HttpStatus.NOT_FOUND)); + } + + @Test + public void availableWithLogFile() throws Exception { + this.environment.setProperty("logging.file", this.logFile.getAbsolutePath()); + assertThat(this.mvc.available().getStatusCode(), equalTo(HttpStatus.OK)); + } + + @Test + public void notAvailableIfDisabled() throws Exception { + this.environment.setProperty("logging.file", this.logFile.getAbsolutePath()); + this.mvc.setEnabled(false); + assertThat(this.mvc.available().getStatusCode(), equalTo(HttpStatus.NOT_FOUND)); + } + + @Test + public void invokeGetsContent() throws IOException { + this.environment.setProperty("logging.file", this.logFile.getAbsolutePath()); + ResponseEntity response = this.mvc.invoke(); + assertEquals(HttpStatus.OK, response.getStatusCode()); + InputStream inputStream = ((Resource) response.getBody()).getInputStream(); + InputStreamReader reader = new InputStreamReader(inputStream); + assertEquals("--TEST--", FileCopyUtils.copyToString(reader)); + } + +} diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 2e0b654217..ce74a4bd8b 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -643,6 +643,9 @@ content into your application; rather pick only the properties that you need. endpoints.info.id=info endpoints.info.sensitive=false endpoints.info.enabled=true + endpoints.logfile.path=/logfile + endpoints.logfile.sensitive=true + endpoints.logfile.enabled=true endpoints.mappings.enabled=true endpoints.mappings.id=mappings endpoints.mappings.sensitive=true 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 8fb7e08706..ca7fc8d863 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -94,6 +94,11 @@ unauthenticated connection or full message details when authenticated). |Displays arbitrary application info. |false +|`logfile` +|Returns the contents of the logfile (if `logging.file` or `logging.path` properties have +been set). Only available via MVC. +|true + |`metrics` |Shows '`metrics`' information for the current application. |true