diff --git a/pom.xml b/pom.xml index 1155820d8d3dbcc7244bbb6756078ae27767153b..e14879fad321e2cffccf44f3ca17fc8b200a271d 100644 --- a/pom.xml +++ b/pom.xml @@ -213,6 +213,18 @@ <groupId>org.springframework.security</groupId> <artifactId>spring-security-ldap</artifactId> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-actuator-autoconfigure</artifactId> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-jmx</artifactId> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + </dependency> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> diff --git a/src/main/java/de/thm/arsnova/config/AppConfig.java b/src/main/java/de/thm/arsnova/config/AppConfig.java index 4b14d85ad2a24aa00fc7bc7aec8c15289ed2fb04..880574ba10b03dbadbb5d562e79ee9fb7a62974a 100644 --- a/src/main/java/de/thm/arsnova/config/AppConfig.java +++ b/src/main/java/de/thm/arsnova/config/AppConfig.java @@ -26,6 +26,9 @@ import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; @@ -69,7 +72,7 @@ import de.thm.arsnova.util.ImageUtils; import de.thm.arsnova.web.CacheControlInterceptorHandler; import de.thm.arsnova.web.CorsFilter; import de.thm.arsnova.web.DeprecatedApiInterceptorHandler; -import de.thm.arsnova.web.PathApiVersionContentNegotiationStrategy; +import de.thm.arsnova.web.PathBasedContentNegotiationStrategy; import de.thm.arsnova.web.ResponseInterceptorHandler; import de.thm.arsnova.websocket.ArsnovaSocketioServer; import de.thm.arsnova.websocket.ArsnovaSocketioServerImpl; @@ -87,18 +90,23 @@ import de.thm.arsnova.websocket.ArsnovaSocketioServerImpl; "de.thm.arsnova.cache", "de.thm.arsnova.controller", "de.thm.arsnova.event", + "de.thm.arsnova.management", "de.thm.arsnova.security", "de.thm.arsnova.service", "de.thm.arsnova.web", "de.thm.arsnova.websocket.handler"}) @Configuration @EnableAsync(mode = AdviceMode.ASPECTJ) +@EnableAutoConfiguration @EnableCaching(mode = AdviceMode.ASPECTJ) @EnableScheduling @EnableSpringConfigured @EnableWebMvc @PropertySource( - value = {"classpath:config/defaults.yml", "file:${arsnova.config-dir:.}/application.yml"}, + value = { + "classpath:config/defaults.yml", + "classpath:config/actuator.yml", + "file:${arsnova.config-dir:.}/application.yml"}, ignoreResourceNotFound = true, encoding = "UTF-8", factory = YamlPropertySourceFactory.class @@ -109,6 +117,7 @@ public class AppConfig implements WebMvcConfigurer { public static final String API_V3_MEDIA_TYPE_VALUE = "application/vnd.de.thm.arsnova.v3+json"; public static final MediaType API_V2_MEDIA_TYPE = MediaType.valueOf(API_V2_MEDIA_TYPE_VALUE); public static final MediaType API_V3_MEDIA_TYPE = MediaType.valueOf(API_V3_MEDIA_TYPE_VALUE); + public static final MediaType ACTUATOR_MEDIA_TYPE = MediaType.valueOf(ActuatorMediaType.V2_JSON); @Autowired private Environment env; @@ -119,18 +128,22 @@ public class AppConfig implements WebMvcConfigurer { @Autowired private SecurityProperties securityProperties; + @Autowired + private WebEndpointProperties webEndpointProperties; + @Override public void configureMessageConverters(final List<HttpMessageConverter<?>> converters) { converters.add(defaultJsonMessageConverter()); converters.add(apiV2JsonMessageConverter()); + converters.add(managementJsonMessageConverter()); converters.add(stringMessageConverter()); //converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())); } @Override public void configureContentNegotiation(final ContentNegotiationConfigurer configurer) { - final PathApiVersionContentNegotiationStrategy strategy = - new PathApiVersionContentNegotiationStrategy(API_V3_MEDIA_TYPE); + final PathBasedContentNegotiationStrategy strategy = + new PathBasedContentNegotiationStrategy(API_V3_MEDIA_TYPE, webEndpointProperties.getBasePath()); configurer.mediaType("json", MediaType.APPLICATION_JSON_UTF8); configurer.mediaType("xml", MediaType.APPLICATION_XML); configurer.favorParameter(false); @@ -228,6 +241,22 @@ public class AppConfig implements WebMvcConfigurer { return converter; } + @Bean + public MappingJackson2HttpMessageConverter managementJsonMessageConverter() { + final Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); + builder + .indentOutput(systemProperties.getApi().isIndentResponseBody()) + .simpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + final ObjectMapper mapper = builder.build(); + final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(mapper); + final List<MediaType> mediaTypes = new ArrayList<>(); + mediaTypes.add(ACTUATOR_MEDIA_TYPE); + mediaTypes.add(MediaType.APPLICATION_JSON_UTF8); + converter.setSupportedMediaTypes(mediaTypes); + + return converter; + } + @Bean public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { final PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); diff --git a/src/main/java/de/thm/arsnova/controller/WelcomeController.java b/src/main/java/de/thm/arsnova/controller/WelcomeController.java index 8ce005e5084572e210ef09a8a299c45576ef8296..59c32288119caecd187a47e055c4675149899207 100644 --- a/src/main/java/de/thm/arsnova/controller/WelcomeController.java +++ b/src/main/java/de/thm/arsnova/controller/WelcomeController.java @@ -22,11 +22,9 @@ import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.net.UnknownHostException; -import java.util.HashMap; import java.util.Map; -import java.util.Properties; -import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -42,6 +40,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.View; import org.springframework.web.servlet.view.RedirectView; +import de.thm.arsnova.management.VersionInfoContributor; import de.thm.arsnova.web.exceptions.BadRequestException; import de.thm.arsnova.web.exceptions.NoContentException; @@ -54,8 +53,8 @@ public class WelcomeController extends AbstractController { @Value("${mobile.path}") private String mobileContextPath; - @Resource(name = "versionInfoProperties") - private Properties versionInfoProperties; + @Autowired + private VersionInfoContributor versionInfoContributor; @RequestMapping(value = "/", method = RequestMethod.GET) public View home() { @@ -65,18 +64,7 @@ public class WelcomeController extends AbstractController { @RequestMapping(value = "/", method = RequestMethod.GET, produces = "application/json") @ResponseBody public Map<String, Object> jsonHome() { - final Map<String, Object> response = new HashMap<>(); - final Map<String, Object> version = new HashMap<>(); - - version.put("string", versionInfoProperties.getProperty("version.string")); - version.put("buildTime", versionInfoProperties.getProperty("version.build-time")); - version.put("gitCommitId", versionInfoProperties.getProperty("version.git.commit-id")); - version.put("gitDirty", Boolean.parseBoolean(versionInfoProperties.getProperty("version.git.dirty"))); - - response.put("productName", "arsnova-backend"); - response.put("version", version); - - return response; + return versionInfoContributor.getInfoDetails(); } @RequestMapping(value = "/checkframeoptionsheader", method = RequestMethod.POST) diff --git a/src/main/java/de/thm/arsnova/management/StatisticsEndpoint.java b/src/main/java/de/thm/arsnova/management/StatisticsEndpoint.java new file mode 100644 index 0000000000000000000000000000000000000000..084972f475cf31a10f1674bb8f29565c64c82a15 --- /dev/null +++ b/src/main/java/de/thm/arsnova/management/StatisticsEndpoint.java @@ -0,0 +1,41 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2019 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.thm.arsnova.management; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.stereotype.Component; + +import de.thm.arsnova.model.Statistics; +import de.thm.arsnova.service.StatisticsService; + +@Component +@Endpoint(id = "stats") +public class StatisticsEndpoint { + private StatisticsService statisticsService; + + public StatisticsEndpoint(final StatisticsService statisticsService) { + this.statisticsService = statisticsService; + } + + @ReadOperation + public Statistics readStatistics() { + return statisticsService.getStatistics(); + } +} diff --git a/src/main/java/de/thm/arsnova/management/VersionInfoContributor.java b/src/main/java/de/thm/arsnova/management/VersionInfoContributor.java new file mode 100644 index 0000000000000000000000000000000000000000..8a7defe350b7386095dc4461203dbcf8baa53e09 --- /dev/null +++ b/src/main/java/de/thm/arsnova/management/VersionInfoContributor.java @@ -0,0 +1,55 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2019 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.thm.arsnova.management; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import javax.annotation.Resource; +import org.springframework.boot.actuate.info.Info; +import org.springframework.boot.actuate.info.InfoContributor; +import org.springframework.stereotype.Component; + +@Component +public class VersionInfoContributor implements InfoContributor { + private Map<String, Object> infoDetails; + + @Override + public void contribute(final Info.Builder builder) { + builder.withDetails(infoDetails); + } + + @Resource(name = "versionInfoProperties") + public void setVersionInfoProperties(final Properties versionInfoProperties) { + infoDetails = new HashMap<>(); + final Map<String, Object> version = new HashMap<>(); + + version.put("string", versionInfoProperties.getProperty("version.string")); + version.put("buildTime", versionInfoProperties.getProperty("version.build-time")); + version.put("gitCommitId", versionInfoProperties.getProperty("version.git.commit-id")); + version.put("gitDirty", Boolean.parseBoolean(versionInfoProperties.getProperty("version.git.dirty"))); + + infoDetails.put("productName", "arsnova-backend"); + infoDetails.put("version", version); + } + + public Map<String, Object> getInfoDetails() { + return infoDetails; + } +} diff --git a/src/main/java/de/thm/arsnova/web/PathApiVersionContentNegotiationStrategy.java b/src/main/java/de/thm/arsnova/web/PathBasedContentNegotiationStrategy.java similarity index 82% rename from src/main/java/de/thm/arsnova/web/PathApiVersionContentNegotiationStrategy.java rename to src/main/java/de/thm/arsnova/web/PathBasedContentNegotiationStrategy.java index b5875482a84979586104c51d2253d3825f793882..a7e7dd5869fccce322fb8665898ae92ae7e24fa0 100644 --- a/src/main/java/de/thm/arsnova/web/PathApiVersionContentNegotiationStrategy.java +++ b/src/main/java/de/thm/arsnova/web/PathBasedContentNegotiationStrategy.java @@ -39,14 +39,17 @@ import de.thm.arsnova.controller.AbstractEntityController; * * @author Daniel Gerhardt */ -public class PathApiVersionContentNegotiationStrategy implements ContentNegotiationStrategy { - private static final Logger logger = LoggerFactory.getLogger(PathApiVersionContentNegotiationStrategy.class); +public class PathBasedContentNegotiationStrategy implements ContentNegotiationStrategy { + private static final Logger logger = LoggerFactory.getLogger(PathBasedContentNegotiationStrategy.class); + + private final String managementPath; private MediaType fallback; private MediaType empty = MediaType.valueOf(AbstractEntityController.MEDIATYPE_EMPTY); - public PathApiVersionContentNegotiationStrategy(final MediaType fallback) { + public PathBasedContentNegotiationStrategy(final MediaType fallback, final String managementPath) { this.fallback = fallback; + this.managementPath = managementPath + "/"; } @Override @@ -54,7 +57,11 @@ public class PathApiVersionContentNegotiationStrategy implements ContentNegotiat throws HttpMediaTypeNotAcceptableException { final HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); final List<MediaType> mediaTypes = new ArrayList<>(); - if (servletRequest.getServletPath().startsWith("/v2/")) { + if (servletRequest.getServletPath().startsWith(managementPath)) { + logger.trace("Negotiating content based on path for management API"); + mediaTypes.add(AppConfig.ACTUATOR_MEDIA_TYPE); + mediaTypes.add(MediaType.TEXT_PLAIN); + } else if (servletRequest.getServletPath().startsWith("/v2/")) { logger.trace("Negotiating content based on path for API v2"); mediaTypes.add(AppConfig.API_V2_MEDIA_TYPE); mediaTypes.add(MediaType.TEXT_PLAIN); diff --git a/src/main/resources/config/actuator.yml b/src/main/resources/config/actuator.yml new file mode 100644 index 0000000000000000000000000000000000000000..568e51f7ff1ea79de1f20d6a69ddeb17de4e0fdd --- /dev/null +++ b/src/main/resources/config/actuator.yml @@ -0,0 +1,11 @@ +arsnova: + management: + endpoints: + web: + base-path: /management + exposure: + include: "*" + metrics: + web: + server: + auto-time-requests: true