diff --git a/pom.xml b/pom.xml index 6d8ae36acc222f7d42dede0340d52010edb36268..9298671601f1680f1781e344ae054e0a0f6a5874 100644 --- a/pom.xml +++ b/pom.xml @@ -219,6 +219,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> @@ -395,11 +407,6 @@ <artifactId>swagger-annotations</artifactId> <version>1.5.22</version> </dependency> - <dependency> - <groupId>com.codahale.metrics</groupId> - <artifactId>metrics-annotation</artifactId> - <version>3.0.2</version> - </dependency> <dependency> <groupId>org.checkerframework</groupId> <artifactId>checker-qual</artifactId> @@ -441,6 +448,12 @@ <source>1.8</source> <target>1.8</target> <aspectLibraries> + <!-- Disabled for now, see https://github.com/micrometer-metrics/micrometer/issues/1149. + <aspectLibrary> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-core</artifactId> + </aspectLibrary> + --> <aspectLibrary> <groupId>org.springframework</groupId> <artifactId>spring-aspects</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..1e992cb6dfbec9909b35417a56a4646f5b2ad3e6 100644 --- a/src/main/java/de/thm/arsnova/config/AppConfig.java +++ b/src/main/java/de/thm/arsnova/config/AppConfig.java @@ -21,11 +21,20 @@ package de.thm.arsnova.config; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import io.micrometer.core.instrument.MeterRegistry; import java.nio.charset.Charset; 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.autoconfigure.metrics.MetricsProperties; +import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter; +import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; @@ -69,7 +78,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 +96,25 @@ 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(exclude = { + DataSourceAutoConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class}) @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 +125,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 +136,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); @@ -161,6 +182,15 @@ public class AppConfig implements WebMvcConfigurer { registry.addResourceHandler("swagger.json").addResourceLocations("classpath:/"); } + /* Provides a Spring Framework (non-Boot) compatible Filter. */ + @Bean + public WebMvcMetricsFilter webMvcMetricsFilterOverride( + final MeterRegistry registry, final WebMvcTagsProvider tagsProvider) { + final MetricsProperties.Web.Server serverProperties = new MetricsProperties.Web.Server(); + return new WebMvcMetricsFilter(registry, tagsProvider, + serverProperties.getRequestsMetricName(), serverProperties.isAutoTimeRequests()); + } + @Bean public CacheControlInterceptorHandler cacheControlInterceptorHandler() { return new CacheControlInterceptorHandler(); @@ -228,6 +258,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/config/AppInitializer.java b/src/main/java/de/thm/arsnova/config/AppInitializer.java index 74e069a743af0bc36b2bebd1e8cd6df9426a6c7b..d1971121822470ca38242df3ed029b9237922407 100644 --- a/src/main/java/de/thm/arsnova/config/AppInitializer.java +++ b/src/main/java/de/thm/arsnova/config/AppInitializer.java @@ -50,11 +50,13 @@ public class AppInitializer extends AbstractAnnotationConfigDispatcherServletIni @Override protected Filter[] getServletFilters() { final CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter("UTF-8"); + final DelegatingFilterProxy webMvcMetricsFilter = new DelegatingFilterProxy("webMvcMetricsFilterOverride"); final DelegatingFilterProxy corsFilter = new DelegatingFilterProxy("corsFilter"); final DelegatingFilterProxy maintenanceModeFilter = new DelegatingFilterProxy("maintenanceModeFilter"); final DelegatingFilterProxy v2ContentTypeOverrideFilter = new DelegatingFilterProxy("v2ContentTypeOverrideFilter"); return new Filter[] { + webMvcMetricsFilter, characterEncodingFilter, corsFilter, maintenanceModeFilter, diff --git a/src/main/java/de/thm/arsnova/config/SecurityConfig.java b/src/main/java/de/thm/arsnova/config/SecurityConfig.java index 47a6df2adbd54e873353f5dfefb56804c72683e7..cb327b08e0ae5208230a5d6d3c7c4f530f144100 100644 --- a/src/main/java/de/thm/arsnova/config/SecurityConfig.java +++ b/src/main/java/de/thm/arsnova/config/SecurityConfig.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.List; import javax.annotation.PostConstruct; import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletResponse; import org.jasig.cas.client.validation.Cas20ProxyTicketValidator; import org.pac4j.core.client.Client; import org.pac4j.core.config.Config; @@ -32,15 +33,21 @@ import org.pac4j.oidc.client.OidcClient; import org.pac4j.oidc.config.OidcConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.security.access.intercept.RunAsManager; import org.springframework.security.access.intercept.RunAsManagerImpl; @@ -71,7 +78,7 @@ import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; @@ -82,6 +89,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import de.thm.arsnova.config.properties.AuthenticationProviderProperties; import de.thm.arsnova.config.properties.SecurityProperties; import de.thm.arsnova.config.properties.SystemProperties; +import de.thm.arsnova.controller.ControllerExceptionHelper; import de.thm.arsnova.security.CasLogoutSuccessHandler; import de.thm.arsnova.security.CasUserDetailsService; import de.thm.arsnova.security.CustomLdapUserDetailsMapper; @@ -126,9 +134,20 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { } public class HttpSecurityConfig extends WebSecurityConfigurerAdapter { + protected AuthenticationEntryPoint authenticationEntryPoint; + protected AccessDeniedHandler accessDeniedHandler; + + public HttpSecurityConfig(final AuthenticationEntryPoint authenticationEntryPoint, + final AccessDeniedHandler accessDeniedHandler) { + this.authenticationEntryPoint = authenticationEntryPoint; + this.accessDeniedHandler = accessDeniedHandler; + } + @Override protected void configure(final HttpSecurity http) throws Exception { - http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint()); + http.exceptionHandling() + .authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler); http.csrf().disable(); http.headers().addHeaderWriter(new HstsHeaderWriter(false)); @@ -149,6 +168,12 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Order(2) @Profile("!test") public class StatelessHttpSecurityConfig extends HttpSecurityConfig { + public StatelessHttpSecurityConfig( + @Qualifier("restAuthenticationEntryPoint") final AuthenticationEntryPoint authenticationEntryPoint, + final AccessDeniedHandler accessDeniedHandler) { + super(authenticationEntryPoint, accessDeniedHandler); + } + @Override protected void configure(final HttpSecurity http) throws Exception { super.configure(http); @@ -161,6 +186,12 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Order(1) @Profile("!test") public class StatefulHttpSecurityConfig extends HttpSecurityConfig { + public StatefulHttpSecurityConfig( + @Qualifier("restAuthenticationEntryPoint") final AuthenticationEntryPoint authenticationEntryPoint, + final AccessDeniedHandler accessDeniedHandler) { + super(authenticationEntryPoint, accessDeniedHandler); + } + @Override protected void configure(final HttpSecurity http) throws Exception { super.configure(http); @@ -169,6 +200,29 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { } } + @Configuration + @Order(Ordered.HIGHEST_PRECEDENCE) + @Profile("!test") + public class ManagementHttpSecurityConfig extends HttpSecurityConfig { + private final String managementPath; + + public ManagementHttpSecurityConfig( + @Qualifier("restAuthenticationEntryPoint") final AuthenticationEntryPoint authenticationEntryPoint, + final AccessDeniedHandler accessDeniedHandler, + final WebEndpointProperties webEndpointProperties) { + super(authenticationEntryPoint, accessDeniedHandler); + this.managementPath = webEndpointProperties.getBasePath(); + } + + @Override + protected void configure(final HttpSecurity http) throws Exception { + super.configure(http); + http.antMatcher(managementPath + "/**"); + http.authorizeRequests().anyRequest().hasRole("ADMIN"); + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } + } + @Configuration @EnableGlobalMethodSecurity(mode = AdviceMode.ASPECTJ, prePostEnabled = true, securedEnabled = true) public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { @@ -247,8 +301,29 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { } @Bean - public static AuthenticationEntryPoint restAuthenticationEntryPoint() { - return new Http403ForbiddenEntryPoint(); + public static AuthenticationEntryPoint restAuthenticationEntryPoint( + @Qualifier("defaultJsonMessageConverter") + final MappingJackson2HttpMessageConverter jackson2HttpMessageConverter, + final ControllerExceptionHelper controllerExceptionHelper) { + return (request, response, accessDeniedException) -> { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType(MediaType.APPLICATION_JSON_UTF8.toString()); + response.getWriter().write(jackson2HttpMessageConverter.getObjectMapper().writeValueAsString( + controllerExceptionHelper.handleException(accessDeniedException, Level.DEBUG))); + }; + } + + @Bean + public AccessDeniedHandler customAccessDeniedHandler( + @Qualifier("defaultJsonMessageConverter") + final MappingJackson2HttpMessageConverter jackson2HttpMessageConverter, + final ControllerExceptionHelper controllerExceptionHelper) { + return (request, response, accessDeniedException) -> { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType(MediaType.APPLICATION_JSON_UTF8.toString()); + response.getWriter().write(jackson2HttpMessageConverter.getObjectMapper().writeValueAsString( + controllerExceptionHelper.handleException(accessDeniedException, Level.DEBUG))); + }; } @Bean diff --git a/src/main/java/de/thm/arsnova/controller/ControllerExceptionHelper.java b/src/main/java/de/thm/arsnova/controller/ControllerExceptionHelper.java index 27d90282cbfcd8a84831026d75a55e32aef16c36..82cca7351522fa4b9ee213dc8e00a19c4950c06f 100644 --- a/src/main/java/de/thm/arsnova/controller/ControllerExceptionHelper.java +++ b/src/main/java/de/thm/arsnova/controller/ControllerExceptionHelper.java @@ -39,7 +39,7 @@ public class ControllerExceptionHelper { this.exposeMessages = systemProperties.getApi().isExposeErrorMessages(); } - protected Map<String, Object> handleException(@NonNull final Throwable e, @NonNull final Level level) { + public Map<String, Object> handleException(@NonNull final Throwable e, @NonNull final Level level) { final String message = e.getMessage() != null ? e.getMessage() : ""; log(level, message, e); final Map<String, Object> result = new HashMap<>(); 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/service/UserServiceImpl.java b/src/main/java/de/thm/arsnova/service/UserServiceImpl.java index 48ed77f620132206fcb7d64e94a08e18f1419e04..9c399600660ef1fa1f39fe7f77bde2bd2878a4e0 100644 --- a/src/main/java/de/thm/arsnova/service/UserServiceImpl.java +++ b/src/main/java/de/thm/arsnova/service/UserServiceImpl.java @@ -18,7 +18,6 @@ package de.thm.arsnova.service; -import com.codahale.metrics.annotation.Gauge; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; @@ -66,7 +65,6 @@ import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.Validator; import org.springframework.web.util.UriUtils; -import org.stagemonitor.core.metrics.MonitorGauges; import de.thm.arsnova.config.properties.AuthenticationProviderProperties; import de.thm.arsnova.config.properties.SecurityProperties; @@ -87,7 +85,6 @@ import de.thm.arsnova.web.exceptions.UnauthorizedException; * Performs all user related operations. */ @Service -@MonitorGauges public class UserServiceImpl extends DefaultEntityServiceImpl<UserProfile> implements UserService { private static final int LOGIN_TRY_RESET_DELAY_MS = 30 * 1000; @@ -324,7 +321,6 @@ public class UserServiceImpl extends DefaultEntityServiceImpl<UserProfile> imple } @Override - @Gauge public int loggedInUsers() { return userIdToRoomId.size(); } 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/java/de/thm/arsnova/websocket/ArsnovaSocketioServerImpl.java b/src/main/java/de/thm/arsnova/websocket/ArsnovaSocketioServerImpl.java index 4717df44f9683b4b0e45e25702e09e5346ecf3ad..d74a42f50d02de163b99d8ee7ec7dd08b7242949 100644 --- a/src/main/java/de/thm/arsnova/websocket/ArsnovaSocketioServerImpl.java +++ b/src/main/java/de/thm/arsnova/websocket/ArsnovaSocketioServerImpl.java @@ -18,7 +18,6 @@ package de.thm.arsnova.websocket; -import com.codahale.metrics.annotation.Timed; import com.corundumstudio.socketio.AckRequest; import com.corundumstudio.socketio.Configuration; import com.corundumstudio.socketio.SocketConfig; @@ -29,6 +28,7 @@ import com.corundumstudio.socketio.listener.DataListener; import com.corundumstudio.socketio.listener.DisconnectListener; import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.protocol.PacketType; +import io.micrometer.core.annotation.Timed; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -153,7 +153,7 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer { server.addEventListener("setFeedback", Feedback.class, new DataListener<Feedback>() { @Override - @Timed(name = "setFeedbackEvent.onData") + @Timed("setFeedbackEvent.onData") public void onData(final SocketIOClient client, final Feedback data, final AckRequest ackSender) { final String userId = userService.getUserIdToSocketId(client.getSessionId()); if (userId == null) { @@ -177,7 +177,7 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer { server.addEventListener("setSession", Room.class, new DataListener<Room>() { @Override - @Timed(name = "setSessionEvent.onData") + @Timed("setSessionEvent.onData") public void onData(final SocketIOClient client, final Room room, final AckRequest ackSender) { final String userId = userService.getUserIdToSocketId(client.getSessionId()); if (null == userId) { @@ -211,7 +211,7 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer { Comment.class, new DataListener<Comment>() { @Override - @Timed(name = "readInterposedQuestionEvent.onData") + @Timed("readInterposedQuestionEvent.onData") public void onData( final SocketIOClient client, final Comment comment, @@ -244,7 +244,7 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer { ScoreOptions.class, new DataListener<ScoreOptions>() { @Override - @Timed(name = "setLearningProgressOptionsEvent.onData") + @Timed("setLearningProgressOptionsEvent.onData") public void onData( final SocketIOClient client, final ScoreOptions scoreOptions, final AckRequest ack) { throw new UnsupportedOperationException("Not implemented."); @@ -254,7 +254,7 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer { server.addConnectListener(new ConnectListener() { @Override - @Timed + @Timed("onConnect") public void onConnect(final SocketIOClient client) { } @@ -262,7 +262,7 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer { server.addDisconnectListener(new DisconnectListener() { @Override - @Timed + @Timed("onDisconnect") public void onDisconnect(final SocketIOClient client) { if ( userService == null diff --git a/src/main/java/org/stagemonitor/core/metrics/MonitorGauges.java b/src/main/java/org/stagemonitor/core/metrics/MonitorGauges.java deleted file mode 100644 index 4e1da49e4e0cb5a9446d958ecc8c6621bc8a1330..0000000000000000000000000000000000000000 --- a/src/main/java/org/stagemonitor/core/metrics/MonitorGauges.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.stagemonitor.core.metrics; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * When a type is marked with this annotation, the creation of gauges with - * {@link com.codahale.metrics.annotation.Gauge} is activated for that type. - * - * <pre><code> - * \@MonitorGauges - * public class Queue { - * \@Gauge(name = "queueSize") - * public int getQueueSize() { - * return queue.size; - * } - * } - * </code></pre> - */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface MonitorGauges { -} diff --git a/src/main/resources/META-INF/aop.xml b/src/main/resources/META-INF/aop.xml index fe4871b30ed1a8777f4caccb3c138cc8747a843c..9887fa3ffc05ec45fa30845d1bf7576defd76c93 100644 --- a/src/main/resources/META-INF/aop.xml +++ b/src/main/resources/META-INF/aop.xml @@ -9,5 +9,9 @@ <aspect name="de.thm.arsnova.web.RangeAspect"/> <aspect name="de.thm.arsnova.web.InternalEntityAspect"/> <aspect name="de.thm.arsnova.websocket.WebsocketAuthenticationAspect"/> + <!-- Micrometer does not have a aop.xml config for its aspects. --> + <!-- Disabled for now, see https://github.com/micrometer-metrics/micrometer/issues/1149. + <aspect name="io.micrometer.core.aop.TimedAspect"/> + --> </aspects> </aspectj> 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