diff --git a/build.gradle b/build.gradle index 2e534b56..bc1012cd 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-actuator' + // monitoring + implementation 'io.micronaut.micrometer:micronaut-micrometer-registry-prometheus:5.8.0' + implementation 'org.springframework.boot:spring-boot-starter-actuator:3.3.4' + // springdoc implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springDocVersion}" implementation "org.springdoc:springdoc-openapi-starter-common:${springDocVersion}" diff --git a/settings/application-default.properties b/settings/application-default.properties index e07f6dea..921214cf 100644 --- a/settings/application-default.properties +++ b/settings/application-default.properties @@ -19,7 +19,7 @@ spring.servlet.multipart.max-file-size=100MB spring.servlet.multipart.max-request-size=100MB # Logging settings logging.level.root=ERROR -logging.level.edu.kit.datamanager=INFO +logging.level.edu.kit.datamanager=DEBUG springdoc.swagger-ui.disable-swagger-default-url=true # Actuator settings info.app.name=Mapping-Service @@ -27,9 +27,15 @@ info.app.description=Generic mapping service supporting different mapping implem info.app.group=edu.kit.datamanager info.app.version=1.0.4 management.endpoint.health.probes.enabled=true -management.endpoints.web.exposure.include=* -management.health.rabbit.enabled:false -management.health.elasticsearch.enabled:false +management.endpoint.health.enabled: true +management.endpoint.health.show-details: when-authorized +management.endpoint.health.sensitive: true +management.endpoints.web.exposure.include: health,info + +#spring.security.user.name=admin +#spring.security.user.password=secret +#spring.security.user.roles=ADMIN + ############################################################################### # Spring Cloud ############################################################################### @@ -56,6 +62,10 @@ mapping-service.pluginLocation=file://INSTALLATION_DIR/plugins mapping-service.mappingSchemasLocation=file://INSTALLATION_DIR/mappingSchemas # Folder where job output files for async mapping executions are stored mapping-service.jobOutput=file://INSTALLATION_DIR/jobOutput + +management.metrics.export.prometheus.enabled=true +management.endpoint.metrics.enabled=true + # Execution timeout for script calls mapping-service.executionTimeout=30 diff --git a/src/main/java/edu/kit/datamanager/mappingservice/MappingServiceApplication.java b/src/main/java/edu/kit/datamanager/mappingservice/MappingServiceApplication.java index 42ec2d57..126e6a7f 100644 --- a/src/main/java/edu/kit/datamanager/mappingservice/MappingServiceApplication.java +++ b/src/main/java/edu/kit/datamanager/mappingservice/MappingServiceApplication.java @@ -3,8 +3,9 @@ import com.google.common.collect.ImmutableSet; import com.google.common.reflect.ClassPath; import edu.kit.datamanager.mappingservice.configuration.ApplicationProperties; -import edu.kit.datamanager.mappingservice.plugins.PluginLoader; import edu.kit.datamanager.mappingservice.plugins.PluginManager; +import io.micrometer.core.instrument.MeterRegistry; +import edu.kit.datamanager.mappingservice.plugins.PluginLoader; import edu.kit.datamanager.mappingservice.util.PythonRunnerUtil; import edu.kit.datamanager.mappingservice.util.ShellRunnerUtil; import edu.kit.datamanager.security.filter.KeycloakJwtProperties; @@ -13,6 +14,7 @@ import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -32,6 +34,13 @@ public class MappingServiceApplication { private static final Logger LOG = LoggerFactory.getLogger(MappingServiceApplication.class); + @Autowired + private final MeterRegistry meterRegistry; + + MappingServiceApplication(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + @Bean public ApplicationProperties applicationProperties() { return new ApplicationProperties(); @@ -46,7 +55,7 @@ public PluginLoader pluginLoader() { public PluginManager pluginManager() { PythonRunnerUtil.init(applicationProperties()); ShellRunnerUtil.init(applicationProperties()); - return new PluginManager(applicationProperties(), pluginLoader()); + return new PluginManager(applicationProperties(), pluginLoader(), meterRegistry); } @Bean diff --git a/src/main/java/edu/kit/datamanager/mappingservice/configuration/StaticResourcesConfiguration.java b/src/main/java/edu/kit/datamanager/mappingservice/configuration/StaticResourcesConfiguration.java index 9ff92c77..948850d3 100644 --- a/src/main/java/edu/kit/datamanager/mappingservice/configuration/StaticResourcesConfiguration.java +++ b/src/main/java/edu/kit/datamanager/mappingservice/configuration/StaticResourcesConfiguration.java @@ -15,7 +15,10 @@ */ package edu.kit.datamanager.mappingservice.configuration; +import edu.kit.datamanager.mappingservice.rest.impl.PreHandleInterceptor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -28,10 +31,16 @@ */ @Configuration public class StaticResourcesConfiguration implements WebMvcConfigurer { + private final PreHandleInterceptor preHandleInterceptor; private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/static/"}; + @Autowired + public StaticResourcesConfiguration(PreHandleInterceptor preHandleInterceptor) { + this.preHandleInterceptor = preHandleInterceptor; + } + @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS); @@ -43,4 +52,9 @@ public void configurePathMatch(PathMatchConfigurer configurer) { urlPathHelper.setUrlDecode(false); configurer.setUrlPathHelper(urlPathHelper); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(preHandleInterceptor); + } } diff --git a/src/main/java/edu/kit/datamanager/mappingservice/configuration/WebSecurityConfig.java b/src/main/java/edu/kit/datamanager/mappingservice/configuration/WebSecurityConfig.java index d01bcb93..73e633ac 100644 --- a/src/main/java/edu/kit/datamanager/mappingservice/configuration/WebSecurityConfig.java +++ b/src/main/java/edu/kit/datamanager/mappingservice/configuration/WebSecurityConfig.java @@ -35,6 +35,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.config.Customizer; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.firewall.DefaultHttpFirewall; @@ -105,6 +106,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { requestMatchers(AUTH_WHITELIST_SWAGGER_UI).permitAll(). anyRequest().authenticated() ). + httpBasic(Customizer.withDefaults()). cors(cors -> cors.configurationSource(corsConfigurationSource())). sessionManagement( session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); diff --git a/src/main/java/edu/kit/datamanager/mappingservice/impl/MappingService.java b/src/main/java/edu/kit/datamanager/mappingservice/impl/MappingService.java index 45336e83..4a75fd80 100644 --- a/src/main/java/edu/kit/datamanager/mappingservice/impl/MappingService.java +++ b/src/main/java/edu/kit/datamanager/mappingservice/impl/MappingService.java @@ -31,6 +31,8 @@ import edu.kit.datamanager.mappingservice.plugins.MappingPluginState; import edu.kit.datamanager.mappingservice.plugins.PluginManager; import edu.kit.datamanager.mappingservice.util.FileUtil; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; import org.apache.commons.codec.binary.Hex; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; @@ -89,7 +91,8 @@ public class MappingService { */ private Path jobsOutputDirectory; - private final ApplicationProperties applicationProperties; + private ApplicationProperties applicationProperties; + private final MeterRegistry meterRegistry; /** * Logger for this class. @@ -97,8 +100,9 @@ public class MappingService { private final static Logger LOGGER = LoggerFactory.getLogger(MappingService.class); @Autowired - public MappingService(ApplicationProperties applicationProperties) { + public MappingService(ApplicationProperties applicationProperties, MeterRegistry meterRegistry) { this.applicationProperties = applicationProperties; + this.meterRegistry = meterRegistry; init(this.applicationProperties); } @@ -212,6 +216,9 @@ public Optional executeMapping(URI contentUrl, String mappingId) throws Ma if (optionalMappingRecord.isPresent()) { LOGGER.trace("Mapping for id {} found.", mappingId); mappingRecord = optionalMappingRecord.get(); + + Counter.builder("mapping_service.plugin_usage").tag("plugin", mappingRecord.getMappingType()).register(meterRegistry).increment(); + Path mappingFile = Paths.get(mappingRecord.getMappingDocumentUri()); // execute mapping Path resultFile; diff --git a/src/main/java/edu/kit/datamanager/mappingservice/plugins/PluginManager.java b/src/main/java/edu/kit/datamanager/mappingservice/plugins/PluginManager.java index 4632a4d9..25d13b74 100644 --- a/src/main/java/edu/kit/datamanager/mappingservice/plugins/PluginManager.java +++ b/src/main/java/edu/kit/datamanager/mappingservice/plugins/PluginManager.java @@ -15,6 +15,8 @@ package edu.kit.datamanager.mappingservice.plugins; import edu.kit.datamanager.mappingservice.configuration.ApplicationProperties; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; import edu.kit.datamanager.mappingservice.exception.MappingServiceException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,10 +64,12 @@ public class PluginManager { * instantiation time. */ @Autowired - public PluginManager(ApplicationProperties applicationProperties, PluginLoader pluginLoader) { + public PluginManager(ApplicationProperties applicationProperties, PluginLoader pluginLoader, MeterRegistry meterRegistry) { this.applicationProperties = applicationProperties; this.pluginLoader = pluginLoader; reloadPlugins(); + + Gauge.builder("mapping_service.plugins_total", () -> plugins.size()).register(meterRegistry); } /** diff --git a/src/main/java/edu/kit/datamanager/mappingservice/rest/impl/MappingAdministrationController.java b/src/main/java/edu/kit/datamanager/mappingservice/rest/impl/MappingAdministrationController.java index 1502e888..25d01da3 100644 --- a/src/main/java/edu/kit/datamanager/mappingservice/rest/impl/MappingAdministrationController.java +++ b/src/main/java/edu/kit/datamanager/mappingservice/rest/impl/MappingAdministrationController.java @@ -28,6 +28,8 @@ import edu.kit.datamanager.mappingservice.rest.PluginInformation; import edu.kit.datamanager.util.AuthenticationHelper; import edu.kit.datamanager.util.ControllerUtils; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; import io.swagger.v3.core.util.Json; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -86,10 +88,12 @@ public class MappingAdministrationController implements IMappingAdministrationCo */ private final MappingService mappingService; - public MappingAdministrationController(IMappingRecordDao mappingRecordDao, PluginManager pluginManager, MappingService mappingService) { + public MappingAdministrationController(IMappingRecordDao mappingRecordDao, PluginManager pluginManager, MappingService mappingService, MeterRegistry meterRegistry) { this.mappingRecordDao = mappingRecordDao; this.mappingService = mappingService; this.pluginManager = pluginManager; + + Gauge.builder("mapping_service.schemes_total", mappingRecordDao::count).register(meterRegistry); } @Override diff --git a/src/main/java/edu/kit/datamanager/mappingservice/rest/impl/MappingExecutionController.java b/src/main/java/edu/kit/datamanager/mappingservice/rest/impl/MappingExecutionController.java index eaab5b3c..d290019a 100644 --- a/src/main/java/edu/kit/datamanager/mappingservice/rest/impl/MappingExecutionController.java +++ b/src/main/java/edu/kit/datamanager/mappingservice/rest/impl/MappingExecutionController.java @@ -32,6 +32,9 @@ import edu.kit.datamanager.mappingservice.plugins.MappingPluginState; import edu.kit.datamanager.mappingservice.rest.IMappingExecutionController; import edu.kit.datamanager.mappingservice.util.FileUtil; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.MeterRegistry; import org.apache.commons.io.FilenameUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,11 +75,17 @@ public class MappingExecutionController implements IMappingExecutionController { private final MappingService mappingService; protected JobManager jobManager; private final IMappingRecordDao mappingRecordDao; + private final MeterRegistry meterRegistry; + private final DistributionSummary documentsInSizeMetric; + private final DistributionSummary documentsOutSizeMetric; - public MappingExecutionController(MappingService mappingService, IMappingRecordDao mappingRecordDao, JobManager jobManager) { + public MappingExecutionController(MappingService mappingService, IMappingRecordDao mappingRecordDao, JobManager jobManager, MeterRegistry meterRegistry) { this.mappingService = mappingService; this.mappingRecordDao = mappingRecordDao; this.jobManager = jobManager; + this.meterRegistry = meterRegistry; + this.documentsInSizeMetric = DistributionSummary.builder("mapping_service.documents.input_size").baseUnit("bytes").register(meterRegistry); + this.documentsOutSizeMetric = DistributionSummary.builder("mapping_service.documents.output_size").baseUnit("bytes").register(meterRegistry); } @Override @@ -161,6 +170,10 @@ public void mapDocument(MultipartFile document, String mappingID, HttpServletReq LOG.error(message, ex); throw new MappingServiceException(message); } finally { + Counter.builder("mapping_service.mapping_usage").tag("mappingID", mappingID).register(meterRegistry).increment(); + this.documentsInSizeMetric.record(document.getSize()); + this.documentsOutSizeMetric.record(result.toFile().length()); + LOG.trace("Result file successfully transferred to client. Removing file {} from disk.", result); try { Files.delete(result); diff --git a/src/main/java/edu/kit/datamanager/mappingservice/rest/impl/PreHandleInterceptor.java b/src/main/java/edu/kit/datamanager/mappingservice/rest/impl/PreHandleInterceptor.java new file mode 100644 index 00000000..37dd3906 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/mappingservice/rest/impl/PreHandleInterceptor.java @@ -0,0 +1,59 @@ +package edu.kit.datamanager.mappingservice.rest.impl; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.security.MessageDigest; +import java.util.HashSet; + +@Service +public class PreHandleInterceptor implements HandlerInterceptor { + private final HashSet uniqueUsers = new HashSet<>(); + private final Counter counter; + + /** + * Logger for this class. + */ + private final static Logger LOGGER = LoggerFactory.getLogger(PreHandleInterceptor.class); + + @Autowired + PreHandleInterceptor(MeterRegistry meterRegistry) { + Gauge.builder("mapping_service.unique_users", uniqueUsers::size).register(meterRegistry); + counter = Counter.builder("mapping_service.requests_served").register(meterRegistry); + } + + @Override + public boolean preHandle(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Object handler) throws Exception { + String forwardedFor = request.getHeader("X-Forwarded-For"); + LOGGER.debug("X-Forwarded-For: {}", forwardedFor); + String clientIp = null; + + if (forwardedFor != null) { + String[] ipList = forwardedFor.split(", "); + if (ipList.length > 0) clientIp = ipList[0]; + LOGGER.debug("Client IP from X-Forwarded-For: {}", clientIp); + } + + String remoteIp = request.getRemoteAddr(); + LOGGER.debug("Client IP from getRemoteAddr: {}", remoteIp); + String ip = clientIp == null ? remoteIp : clientIp; + LOGGER.debug("Using {} for monitoring", ip); + + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(ip.getBytes()); + uniqueUsers.add(new String(messageDigest.digest())); + + counter.increment(); + + return true; + } +} diff --git a/src/test/java/edu/kit/datamanager/mappingservice/TestConfig.java b/src/test/java/edu/kit/datamanager/mappingservice/TestConfig.java index 5e156b73..a61bd5c2 100644 --- a/src/test/java/edu/kit/datamanager/mappingservice/TestConfig.java +++ b/src/test/java/edu/kit/datamanager/mappingservice/TestConfig.java @@ -7,6 +7,8 @@ import edu.kit.datamanager.mappingservice.configuration.ApplicationProperties; import edu.kit.datamanager.mappingservice.plugins.PluginLoader; import edu.kit.datamanager.mappingservice.plugins.PluginManager; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -18,6 +20,8 @@ @Configuration @ComponentScan("edu.kit.datamanager.mappingservice") public class TestConfig { + @Autowired + private MeterRegistry meterRegistry; @Bean public ApplicationProperties applicationProperties() { @@ -31,6 +35,6 @@ public PluginLoader pluginLoader() { @Bean public PluginManager pluginManager() { - return new PluginManager(applicationProperties(), pluginLoader()); + return new PluginManager(applicationProperties(), pluginLoader(), meterRegistry); } } diff --git a/src/test/java/edu/kit/datamanager/mappingservice/impl/MappingServiceTest.java b/src/test/java/edu/kit/datamanager/mappingservice/impl/MappingServiceTest.java index 15a6601e..c5d23c75 100644 --- a/src/test/java/edu/kit/datamanager/mappingservice/impl/MappingServiceTest.java +++ b/src/test/java/edu/kit/datamanager/mappingservice/impl/MappingServiceTest.java @@ -23,6 +23,7 @@ import edu.kit.datamanager.mappingservice.exception.MappingNotFoundException; import edu.kit.datamanager.mappingservice.exception.MappingServiceException; import edu.kit.datamanager.mappingservice.plugins.MappingPluginException; +import io.micrometer.core.instrument.MeterRegistry; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -75,7 +76,6 @@ @TestPropertySource(properties = {"metastore.indexer.mappingsLocation=file:///tmp/metastore2/mapping"}) //@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class MappingServiceTest { - @Autowired ApplicationProperties applicationProperties; @@ -85,6 +85,9 @@ public class MappingServiceTest { @Autowired MappingService mappingService4Test; + @Autowired + MeterRegistry meterRegistry; + private final static String TEMP_DIR_4_MAPPING = "/tmp/mapping-service/"; @BeforeEach @@ -104,7 +107,7 @@ public void setUp() { @Test public void testConstructor() throws URISyntaxException { - new MappingService(applicationProperties); + new MappingService(applicationProperties, meterRegistry); } @Test @@ -116,7 +119,7 @@ public void testConstructorRelativePath() throws IOException, URISyntaxException ap.setMappingsLocation(relativePath); File file = new File(relativePath.getPath()); assertFalse(file.exists()); - new MappingService(ap); + new MappingService(ap, meterRegistry); assertTrue(file.exists()); FileUtils.deleteDirectory(file); assertFalse(file.exists()); @@ -128,7 +131,7 @@ public void testConstructorRelativePath() throws IOException, URISyntaxException @Test public void testConstructorFailing() throws IOException, URISyntaxException { try { - new MappingService(null); + new MappingService(null, meterRegistry); fail("Expected MappingServiceException"); } catch (MappingServiceException ie) { assertTrue(true); diff --git a/src/test/java/edu/kit/datamanager/mappingservice/python/util/PythonUtilsTest.java b/src/test/java/edu/kit/datamanager/mappingservice/python/util/PythonUtilsTest.java index 1a7bd6ce..245f51b5 100644 --- a/src/test/java/edu/kit/datamanager/mappingservice/python/util/PythonUtilsTest.java +++ b/src/test/java/edu/kit/datamanager/mappingservice/python/util/PythonUtilsTest.java @@ -125,7 +125,7 @@ public void testRun_3args_withNoOutputStreams() { PythonRunnerUtil.runPythonScript(scriptLocation, null, null, arguments); fail("Expected MappingPluginException"); } catch (MappingPluginException e) { - assertEquals(MappingPluginState.StateEnum.INVALID_INPUT, e.getMappingPluginState().getState()); + assertEquals(MappingPluginState.StateEnum.UNKNOWN_ERROR, e.getMappingPluginState().getState()); } } }