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 ebce6a5877e5..2e5b809aae7e 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 @@ -43,6 +43,7 @@ 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.MvcEndpoints; import org.springframework.boot.actuate.endpoint.mvc.ShutdownMvcEndpoint; @@ -51,6 +52,7 @@ import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; @@ -84,6 +86,7 @@ * @author Dave Syer * @author Phillip Webb * @author Christian Dupuis + * @author Johannes Stelzer */ @Configuration @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) @@ -181,6 +184,13 @@ public MetricsMvcEndpoint metricsMvcEndpoint(MetricsEndpoint delegate) { return new MetricsMvcEndpoint(delegate); } + @Bean + @ConditionalOnProperty(prefix = "endpoints.logfile", name = "enabled", matchIfMissing = true) + @ConditionalOnExpression("'${logging.file:}' != '' || '${logging.path:}' != ''") + public LogfileMvcEndpoint logfileMvcEndpoint() { + return new LogfileMvcEndpoint(); + } + @Bean @ConditionalOnBean(ShutdownEndpoint.class) @ConditionalOnProperty(prefix = "endpoints.shutdown", name = "enabled", matchIfMissing = true) 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 000000000000..611a7413ec0a --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LogfileMvcEndpoint.java @@ -0,0 +1,141 @@ +/* + * Copyright 2012-2014 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.http.MediaType; +import org.springframework.http.ResponseEntity; +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 + */ +@ConfigurationProperties(prefix = "endpoints.logfile") +public class LogfileMvcEndpoint implements MvcEndpoint, EnvironmentAware { + + private static final Logger LOGGER = LoggerFactory + .getLogger(LogfileMvcEndpoint.class); + + private String path = "/logfile"; + + private boolean sensitive = true; + + private boolean enabled = true; + + private Environment environment; + + @Override + public boolean isSensitive() { + return this.sensitive; + } + + @Override + public String getPath() { + return this.path; + } + + @Override + @SuppressWarnings("rawtypes") + public Class getEndpointType() { + return null; + } + + public void setPath(String path) { + this.path = path; + } + + public void setSensitive(boolean sensitive) { + this.sensitive = sensitive; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return this.enabled; + } + + @RequestMapping(method = RequestMethod.GET) + @ResponseBody + public ResponseEntity invoke() throws IOException { + if (!isAvailable()) { + return ResponseEntity.notFound().build(); + } + + FileSystemResource file = new FileSystemResource(LogFile.get(this.environment) + .toString()); + + return ResponseEntity + .ok() + .contentType(MediaType.TEXT_PLAIN) + .header("Content-Disposition", + "attachment; filename=\"" + file.getFilename() + "\"").body(file); + } + + @RequestMapping(method = RequestMethod.HEAD) + @ResponseBody + public ResponseEntity available() { + if (isAvailable()) { + return ResponseEntity.ok().build(); + } + else { + return ResponseEntity.notFound().build(); + } + } + + private boolean isAvailable() { + if (!this.enabled) { + return false; + } + + LogFile logFile = LogFile.get(this.environment); + if (logFile == null) { + LOGGER.debug("Logfile download failed for missing property 'logging.file'"); + return false; + } + + if (!new FileSystemResource(logFile.toString()).exists()) { + LOGGER.error("Logfile download failed for missing file at path={}", + logFile.toString()); + return false; + } + + return true; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/LogfileEndpointWebMvcAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/LogfileEndpointWebMvcAutoConfigurationTests.java new file mode 100644 index 000000000000..31bad473ed0b --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/LogfileEndpointWebMvcAutoConfigurationTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2014 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.autoconfigure; + +import org.junit.After; +import org.junit.Test; +import org.springframework.boot.actuate.endpoint.mvc.LogfileMvcEndpoint; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link EndpointWebMvcAutoConfiguration} of the {@link LogfileMvcEndpoint}. + * + * @author Johannes Stelzer + */ +public class LogfileEndpointWebMvcAutoConfigurationTests { + + private AnnotationConfigWebApplicationContext context; + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void test_nologfile() throws Exception { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setServletContext(new MockServletContext()); + this.context.register(ManagementServerPropertiesAutoConfiguration.class, + EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class); + this.context.refresh(); + + assertTrue(this.context.getBeansOfType(LogfileMvcEndpoint.class).isEmpty()); + } + + @Test + public void test_logfile() throws Exception { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setServletContext(new MockServletContext()); + this.context.register(ManagementServerPropertiesAutoConfiguration.class, + EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class); + + EnvironmentTestUtils.addEnvironment(this.context, "logging.file:test.log"); + this.context.refresh(); + + assertNotNull(this.context.getBean(LogfileMvcEndpoint.class)); + } + + @Test + public void test_logpath() throws Exception { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setServletContext(new MockServletContext()); + this.context.register(ManagementServerPropertiesAutoConfiguration.class, + EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class); + + EnvironmentTestUtils.addEnvironment(this.context, "logging.path:/var/log"); + this.context.refresh(); + + assertNotNull(this.context.getBean(LogfileMvcEndpoint.class)); + } + +} 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 000000000000..ad65479e4c31 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LogfileMvcEndpointTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2013-2014 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.Test; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.FileCopyUtils; + +import static org.junit.Assert.assertEquals; + +/** + * @author Johannes Stelzer + */ +public class LogfileMvcEndpointTests { + + private LogfileMvcEndpoint mvc = new LogfileMvcEndpoint(); + + @Before + public void before() throws IOException { + FileCopyUtils.copy("--TEST--".getBytes(), new File("test.log")); + } + + @After + public void after() { + new File("test.log").delete(); + } + + @Test + public void available() throws IOException { + MockEnvironment env = new MockEnvironment(); + this.mvc.setEnvironment(env); + + assertEquals(HttpStatus.NOT_FOUND, this.mvc.available().getStatusCode()); + + env.setProperty("logging.file", "no_test.log"); + assertEquals(HttpStatus.NOT_FOUND, this.mvc.available().getStatusCode()); + + env.setProperty("logging.file", "test.log"); + assertEquals(HttpStatus.OK, this.mvc.available().getStatusCode()); + + this.mvc.setEnabled(false); + assertEquals(HttpStatus.NOT_FOUND, this.mvc.available().getStatusCode()); + } + + @Test + public void invoke() throws IOException { + MockEnvironment env = new MockEnvironment(); + this.mvc.setEnvironment(env); + + env.setProperty("logging.file", "test.log"); + ResponseEntity response = this.mvc.invoke(); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("attachment; filename=\"test.log\"", + response.getHeaders().get("Content-Disposition").get(0)); + + InputStream inputStream = ((FileSystemResource) response.getBody()) + .getInputStream(); + assertEquals("--TEST--", + FileCopyUtils.copyToString(new InputStreamReader(inputStream))); + } +}