From d5190dac232f8e9b4cbe7901f07f6c97d98ecf29 Mon Sep 17 00:00:00 2001 From: Daniel Gerhardt <code@dgerhardt.net> Date: Sun, 28 Jul 2019 18:53:18 +0200 Subject: [PATCH] Add /management endpoint provided via Spring Actuator The new endpoint provide metrics including information about application environment, configuration, beans, caches via Spring Actuator. Additional custom endpoints for version info and statistics are available. To allow monitoring of metrics, a Prometheus endpoint is provided via Micrometer. --- pom.xml | 12 ++++ .../java/de/thm/arsnova/config/AppConfig.java | 37 +++++++++++-- .../arsnova/controller/WelcomeController.java | 22 ++------ .../management/StatisticsEndpoint.java | 41 ++++++++++++++ .../management/VersionInfoContributor.java | 55 +++++++++++++++++++ ... PathBasedContentNegotiationStrategy.java} | 15 +++-- src/main/resources/config/actuator.yml | 11 ++++ 7 files changed, 168 insertions(+), 25 deletions(-) create mode 100644 src/main/java/de/thm/arsnova/management/StatisticsEndpoint.java create mode 100644 src/main/java/de/thm/arsnova/management/VersionInfoContributor.java rename src/main/java/de/thm/arsnova/web/{PathApiVersionContentNegotiationStrategy.java => PathBasedContentNegotiationStrategy.java} (82%) create mode 100644 src/main/resources/config/actuator.yml diff --git a/pom.xml b/pom.xml index 1155820d8..e14879fad 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 4b14d85ad..880574ba1 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 8ce005e50..59c322881 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 000000000..084972f47 --- /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 000000000..8a7defe35 --- /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 b5875482a..a7e7dd586 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 000000000..568e51f7f --- /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 -- GitLab