diff --git a/Dockerfile b/Dockerfile
index 5d9a1d4f60cd7487c9de71a8f0057930e7637841..945df88d49e577d182447daed8b95b3caa8074d0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
FROM ibm-semeru-runtimes:open-17.0.7_7-jdk
# RUN mkdir /opt/shareclasses
RUN mkdir -p /opt/openslice/lib/
-COPY target/org.etsi.osl.tmf.api-1.1.0-exec.jar /opt/openslice/lib/
-CMD ["java", "-Xshareclasses:cacheDir=/opt/shareclasses", "-jar", "/opt/openslice/lib/org.etsi.osl.tmf.api-1.1.0-exec.jar"]
+COPY target/org.etsi.osl.tmf.api-1.2.0-exec.jar /opt/openslice/lib/
+CMD ["java", "-Xshareclasses:cacheDir=/opt/shareclasses", "-jar", "/opt/openslice/lib/org.etsi.osl.tmf.api-1.2.0-exec.jar"]
EXPOSE 13082
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 3184b3aa8216683e03259ec2967b84fd4309139e..9773329aeeb660ddebe2ed5c14938e0d2118d9b5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.etsi.osl
org.etsi.osl.main
- 2024Q4
+ 2025Q2
../org.etsi.osl.main
@@ -305,7 +305,7 @@
com.h2database
h2
- test
+ 2.3.232
org.apache.activemq
diff --git a/src/main/java/org/etsi/osl/tmf/BootstrapRepository.java b/src/main/java/org/etsi/osl/tmf/BootstrapRepository.java
index 31cc63fd735f194dbf570f77dc2ad9eb402ab9b1..a7488387094262b84dc577e2a7b55dbffcaf488c 100644
--- a/src/main/java/org/etsi/osl/tmf/BootstrapRepository.java
+++ b/src/main/java/org/etsi/osl/tmf/BootstrapRepository.java
@@ -113,7 +113,7 @@ public class BootstrapRepository {
} else { //check if we have the latest version of GST
if (ADDGST) {
ServiceCategory scategory = this.categRepoService.findByName("Generic Services");
- ServiceSpecification serviceSpecificationObj = this.specRepoService.findByNameAndVersion( GST_EXAMPLE_NAME , "5.0.0");
+ ServiceSpecification serviceSpecificationObj = this.specRepoService.findByNameAndVersion( GST_EXAMPLE_NAME , "10.0.0");
if ( ( scategory != null ) && ( serviceSpecificationObj == null ))
{
diff --git a/src/main/java/org/etsi/osl/tmf/configuration/SwaggerDocumentationConfig.java b/src/main/java/org/etsi/osl/tmf/configuration/SwaggerDocumentationConfig.java
index b8e5b11ab5d362a73dc607ebe8a56453a7d04132..796d26a3a83f5b72bd160c3f6664b812b9220294 100644
--- a/src/main/java/org/etsi/osl/tmf/configuration/SwaggerDocumentationConfig.java
+++ b/src/main/java/org/etsi/osl/tmf/configuration/SwaggerDocumentationConfig.java
@@ -830,7 +830,7 @@ public GroupedOpenApi pim637() {
SpringDocUtils.getConfig().replaceWithClass(java.time.OffsetDateTime.class, java.util.Date.class);
return GroupedOpenApi.builder()
- .group("OpensliceLCMRulesspecificationAPI")
+ .group("OpenSliceLCMRulesspecificationAPI")
.addOpenApiCustomizer( this.lcmOpenAPI() )
.packagesToScan("org.etsi.osl.tmf.lcm.api")
.build();
@@ -947,6 +947,39 @@ public GroupedOpenApi pim637() {
.build();
}
+
+
+ /**
+ * Metrics
+ * @return
+ */
+ @Bean
+ public OpenApiCustomizer metricsOpenAPI() {
+ return openApi -> openApi
+ .specVersion( SpecVersion.V30 ).addSecurityItem(new SecurityRequirement().addList("security_auth"))
+ .info(new Info().title("OpenSlice Metrics API")
+ .description("OpenAPI environment for OpenSlice Metrics")
+ .version("4.0.0")
+ .license(new License()
+ .name("Apache 2.0")
+ .url("https://osl.etsi.org")))
+ .externalDocs(new ExternalDocumentation()
+ .description("OpenSlice Metrics")
+ .url("https://osl.etsi.org"));
+ }
+
+ @Bean
+ public GroupedOpenApi metrics(){
+
+ SpringDocUtils.getConfig().replaceWithClass(java.time.LocalDate.class, java.sql.Date.class);
+ SpringDocUtils.getConfig().replaceWithClass(java.time.OffsetDateTime.class, java.util.Date.class);
+ return GroupedOpenApi.builder()
+ .group("OpenSliceMetricsAPI")
+ .addOpenApiCustomizer( this.metricsOpenAPI() )
+ .packagesToScan("org.etsi.osl.tmf.metrics.api")
+ .build();
+
+ }
// @Bean
diff --git a/src/main/java/org/etsi/osl/tmf/metrics/api/GeneralMetricsApi.java b/src/main/java/org/etsi/osl/tmf/metrics/api/GeneralMetricsApi.java
new file mode 100644
index 0000000000000000000000000000000000000000..4c3714c563937337897bf35da9c21f4b9b56ed8e
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/metrics/api/GeneralMetricsApi.java
@@ -0,0 +1,49 @@
+package org.etsi.osl.tmf.metrics.api;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.etsi.osl.tmf.metrics.PublishedServiceSpecifications;
+import org.etsi.osl.tmf.metrics.RegisteredIndividuals;
+import org.etsi.osl.tmf.metrics.RegisteredResourceSpecifications;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import java.util.Map;
+
+@Tag(name = "GeneralMetricsApi", description = "The General Metrics API")
+public interface GeneralMetricsApi {
+
+ Logger log = LoggerFactory.getLogger(GeneralMetricsApi.class);
+
+ @Operation(summary = "Get total number of registered individuals", operationId = "getRegisteredIndividuals")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Success"),
+ @ApiResponse(responseCode = "400", description = "Bad Request"),
+ @ApiResponse(responseCode = "500", description = "Internal Server Error")
+ })
+ @RequestMapping(value = "/metrics/registeredIndividuals", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
+ ResponseEntity getRegisteredIndividuals();
+
+ @Operation(summary = "Get total number of published service specifications", operationId = "getPublishedServiceSpecifications")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Success"),
+ @ApiResponse(responseCode = "400", description = "Bad Request"),
+ @ApiResponse(responseCode = "500", description = "Internal Server Error")
+ })
+ @RequestMapping(value = "/metrics/publishedServiceSpecifications", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
+ ResponseEntity getPublishedServiceSpecifications();
+
+ @Operation(summary = "Get total number of registered resource specifications", operationId = "getRegisteredResourceSpecifications")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Success"),
+ @ApiResponse(responseCode = "400", description = "Bad Request"),
+ @ApiResponse(responseCode = "500", description = "Internal Server Error")
+ })
+ @RequestMapping(value = "/metrics/registeredResourceSpecifications", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
+ ResponseEntity getRegisteredResourceSpecifications();
+}
diff --git a/src/main/java/org/etsi/osl/tmf/metrics/api/GeneralMetricsApiController.java b/src/main/java/org/etsi/osl/tmf/metrics/api/GeneralMetricsApiController.java
new file mode 100644
index 0000000000000000000000000000000000000000..187920b39606571c595d93d3acd804d3ed7c833f
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/metrics/api/GeneralMetricsApiController.java
@@ -0,0 +1,64 @@
+package org.etsi.osl.tmf.metrics.api;
+
+import org.etsi.osl.tmf.metrics.PublishedServiceSpecifications;
+import org.etsi.osl.tmf.metrics.RegisteredIndividuals;
+import org.etsi.osl.tmf.metrics.RegisteredResourceSpecifications;
+import org.etsi.osl.tmf.metrics.reposervices.GeneralMetricsRepoService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Controller
+public class GeneralMetricsApiController implements GeneralMetricsApi {
+
+ private static final Logger log = LoggerFactory.getLogger(GeneralMetricsApiController.class);
+
+ private final GeneralMetricsRepoService generalMetricsRepoService;
+
+ @Autowired
+ public GeneralMetricsApiController(GeneralMetricsRepoService generalMetricsRepoService) {
+ this.generalMetricsRepoService = generalMetricsRepoService;
+ }
+
+ @Override
+ public ResponseEntity getRegisteredIndividuals() {
+ try {
+ int totalIndividuals = generalMetricsRepoService.countRegisteredIndividuals();
+ RegisteredIndividuals response = new RegisteredIndividuals(totalIndividuals);
+ return new ResponseEntity<>(response, HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Couldn't retrieve total registered individuals. ", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity getPublishedServiceSpecifications() {
+ try {
+ int totalSpecifications = generalMetricsRepoService.countPublishedServiceSpecifications();
+ PublishedServiceSpecifications response = new PublishedServiceSpecifications(totalSpecifications);
+ return new ResponseEntity<>(response, HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Couldn't retrieve total published service specifications. ", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity getRegisteredResourceSpecifications() {
+ try {
+ int totalResourceSpecifications = generalMetricsRepoService.countRegisteredResourceSpecifications();
+ RegisteredResourceSpecifications response = new RegisteredResourceSpecifications(totalResourceSpecifications);
+ return new ResponseEntity<>(response, HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Couldn't retrieve total registered resource specifications. ", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+}
diff --git a/src/main/java/org/etsi/osl/tmf/metrics/api/ResourceMetricsApi.java b/src/main/java/org/etsi/osl/tmf/metrics/api/ResourceMetricsApi.java
new file mode 100644
index 0000000000000000000000000000000000000000..8f0016979b961f47f980234faa1366f034d71988
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/metrics/api/ResourceMetricsApi.java
@@ -0,0 +1,49 @@
+package org.etsi.osl.tmf.metrics.api;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.etsi.osl.tmf.metrics.ResourcesGroupByState;
+import org.etsi.osl.tmf.metrics.TotalResources;
+import org.etsi.osl.tmf.ri639.model.ResourceStatusType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.format.annotation.DateTimeFormat;
+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.RequestParam;
+
+import java.time.OffsetDateTime;
+import java.util.Map;
+
+@Tag(name = "ResourceMetricsApi", description = "The Resources' Metrics API")
+public interface ResourceMetricsApi {
+
+ Logger log = LoggerFactory.getLogger(ResourceMetricsApi.class);
+
+ @Operation(summary = "Get total number of resources", operationId = "getTotalResources")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Success"),
+ @ApiResponse(responseCode = "400", description = "Bad Request"),
+ @ApiResponse(responseCode = "500", description = "Internal Server Error")
+ })
+ @RequestMapping(value = "/metrics/totalResources", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
+ ResponseEntity getTotalResources(
+ @Valid @RequestParam(value = "state", required = false) ResourceStatusType state
+ );
+
+ @Operation(summary = "Get resources grouped by state", operationId = "getResourcesGroupedByState")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Success"),
+ @ApiResponse(responseCode = "400", description = "Bad Request"),
+ @ApiResponse(responseCode = "500", description = "Internal Server Error")
+ })
+ @RequestMapping(value = "/metrics/resourcesGroupByState", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
+ ResponseEntity getResourcesGroupedByState(
+ @Valid @RequestParam(value = "starttime", required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime starttime,
+ @Valid @RequestParam(value = "endtime", required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime endtime
+ );
+}
diff --git a/src/main/java/org/etsi/osl/tmf/metrics/api/ResourceMetricsApiController.java b/src/main/java/org/etsi/osl/tmf/metrics/api/ResourceMetricsApiController.java
new file mode 100644
index 0000000000000000000000000000000000000000..fac650828ed2384d72b70a6f6300816a7351a4e9
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/metrics/api/ResourceMetricsApiController.java
@@ -0,0 +1,76 @@
+package org.etsi.osl.tmf.metrics.api;
+
+import org.etsi.osl.tmf.metrics.*;
+import org.etsi.osl.tmf.metrics.reposervices.ResourceMetricsRepoService;
+import org.etsi.osl.tmf.ri639.model.ResourceStatusType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+
+import java.time.OffsetDateTime;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Controller
+public class ResourceMetricsApiController implements ResourceMetricsApi {
+
+ private static final Logger log = LoggerFactory.getLogger(ResourceMetricsApiController.class);
+ private final ResourceMetricsRepoService resourceMetricsRepoService;
+
+ @Autowired
+ public ResourceMetricsApiController(ResourceMetricsRepoService resourceMetricsRepoService) {
+ this.resourceMetricsRepoService = resourceMetricsRepoService;
+ }
+
+ @Override
+ public ResponseEntity getTotalResources(ResourceStatusType state) {
+ try {
+ int totalResources = resourceMetricsRepoService.countTotalResources(state);
+ TotalResources response = new TotalResources(totalResources);
+ return new ResponseEntity<>(response, HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Couldn't retrieve total resources. ", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity getResourcesGroupedByState(OffsetDateTime starttime, OffsetDateTime endtime) {
+ try {
+ Map resourcesByState = resourceMetricsRepoService.getResourcesGroupedByState(starttime, endtime);
+
+ // Initialize with all possible states and 0. Ensures that all states are represented, even if not present in the data.
+ Map fullStateMap = new LinkedHashMap<>();
+ for (ResourceStatusType state : ResourceStatusType.values()) {
+ fullStateMap.put(state.name(), 0); // default to 0
+ }
+
+ // Overwrite counts with actual data
+ resourcesByState.forEach((key, value) -> {
+ fullStateMap.put(key.toUpperCase(), value); // normalize case just in case
+ });
+
+ // Create aggregation items
+ List groupByStateList = fullStateMap.entrySet().stream()
+ .map(entry -> new ResourcesGroupByStateItem(ResourceStatusType.valueOf(entry.getKey()), entry.getValue()))
+ .toList();
+
+ // Build response structure using models
+ ResourcesGroupByStateAggregations aggregations = new ResourcesGroupByStateAggregations(groupByStateList);
+ int total = fullStateMap.values().stream().mapToInt(Integer::intValue).sum();
+ Resources services = new Resources(total, aggregations);
+ ResourcesGroupByState response = new ResourcesGroupByState(services);
+
+ return new ResponseEntity<>(response, HttpStatus.OK);
+
+ } catch (Exception e) {
+ log.error("Couldn't retrieve resources grouped by state. ", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+}
diff --git a/src/main/java/org/etsi/osl/tmf/metrics/api/ServiceMetricsApi.java b/src/main/java/org/etsi/osl/tmf/metrics/api/ServiceMetricsApi.java
new file mode 100644
index 0000000000000000000000000000000000000000..b5595535132a027f88cc6c34e38b8d6f3a2350fe
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/metrics/api/ServiceMetricsApi.java
@@ -0,0 +1,49 @@
+package org.etsi.osl.tmf.metrics.api;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.etsi.osl.tmf.common.model.service.ServiceStateType;
+import org.etsi.osl.tmf.metrics.ServicesGroupByState;
+import org.etsi.osl.tmf.metrics.TotalServices;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.format.annotation.DateTimeFormat;
+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.RequestParam;
+
+import java.time.OffsetDateTime;
+import java.util.Map;
+
+@Tag(name = "ServiceMetricsApi", description = "The Services' Metrics API")
+public interface ServiceMetricsApi {
+
+ Logger log = LoggerFactory.getLogger(ServiceMetricsApi.class);
+
+ @Operation(summary = "Get total number of services", operationId = "getTotalServices")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Success"),
+ @ApiResponse(responseCode = "400", description = "Bad Request"),
+ @ApiResponse(responseCode = "500", description = "Internal Server Error")
+ })
+ @RequestMapping(value = "/metrics/totalServices", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
+ ResponseEntity getTotalServices(
+ @Valid @RequestParam(value = "state", required = false) ServiceStateType state
+ );
+
+ @Operation(summary = "Get services grouped by state", operationId = "getServicesGroupedByState")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Success"),
+ @ApiResponse(responseCode = "400", description = "Bad Request"),
+ @ApiResponse(responseCode = "500", description = "Internal Server Error")
+ })
+ @RequestMapping(value = "/metrics/servicesGroupByState", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
+ ResponseEntity getServicesGroupedByState(
+ @Valid @RequestParam(value = "starttime", required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime starttime,
+ @Valid @RequestParam(value = "endtime", required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime endtime
+ );
+}
diff --git a/src/main/java/org/etsi/osl/tmf/metrics/api/ServiceMetricsApiController.java b/src/main/java/org/etsi/osl/tmf/metrics/api/ServiceMetricsApiController.java
new file mode 100644
index 0000000000000000000000000000000000000000..abe7575e19556a33442e830bdca100173f0d681a
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/metrics/api/ServiceMetricsApiController.java
@@ -0,0 +1,75 @@
+package org.etsi.osl.tmf.metrics.api;
+
+import org.etsi.osl.tmf.common.model.service.ServiceStateType;
+import org.etsi.osl.tmf.metrics.*;
+import org.etsi.osl.tmf.metrics.reposervices.ServiceMetricsRepoService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+
+import java.time.OffsetDateTime;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Controller
+public class ServiceMetricsApiController implements ServiceMetricsApi {
+
+ private static final Logger log = LoggerFactory.getLogger(ServiceMetricsApiController.class);
+ private final ServiceMetricsRepoService serviceMetricsRepoService;
+
+ @Autowired
+ public ServiceMetricsApiController(ServiceMetricsRepoService serviceMetricsRepoService) {
+ this.serviceMetricsRepoService = serviceMetricsRepoService;
+ }
+
+ @Override
+ public ResponseEntity getTotalServices(ServiceStateType state) {
+ try {
+ int totalServices = serviceMetricsRepoService.countTotalServices(state);
+ TotalServices response = new TotalServices(totalServices);
+ return new ResponseEntity<>(response, HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Couldn't retrieve total services. ", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity getServicesGroupedByState(OffsetDateTime starttime, OffsetDateTime endtime) {
+ try {
+ Map servicesByState = serviceMetricsRepoService.getServicesGroupedByState(starttime, endtime);
+
+ // Initialize with all possible states and 0. Ensures that all states are represented, even if not present in the data.
+ Map fullStateMap = new LinkedHashMap<>();
+ for (ServiceStateType state : ServiceStateType.values()) {
+ fullStateMap.put(state.name(), 0); // default to 0
+ }
+
+ // Overwrite counts with actual data
+ servicesByState.forEach((key, value) -> {
+ fullStateMap.put(key.toUpperCase(), value); // normalize case just in case
+ });
+
+ // Create aggregation items
+ List groupByStateList = fullStateMap.entrySet().stream()
+ .map(entry -> new ServicesGroupByStateItem(ServiceStateType.valueOf(entry.getKey()), entry.getValue()))
+ .toList();
+
+ // Build response structure using metrics models
+ ServicesGroupByStateAggregations aggregations = new ServicesGroupByStateAggregations(groupByStateList);
+ int total = fullStateMap.values().stream().mapToInt(Integer::intValue).sum();
+ Services services = new Services(total, aggregations);
+ ServicesGroupByState response = new ServicesGroupByState(services);
+
+ return new ResponseEntity<>(response, HttpStatus.OK);
+
+ } catch (Exception e) {
+ log.error("Couldn't retrieve services grouped by state. ", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+}
diff --git a/src/main/java/org/etsi/osl/tmf/metrics/api/ServiceOrderMetricsApi.java b/src/main/java/org/etsi/osl/tmf/metrics/api/ServiceOrderMetricsApi.java
new file mode 100644
index 0000000000000000000000000000000000000000..17afc56186a9fbbb4282a1dc17b93ef33386be0c
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/metrics/api/ServiceOrderMetricsApi.java
@@ -0,0 +1,75 @@
+package org.etsi.osl.tmf.metrics.api;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.etsi.osl.tmf.metrics.ActiveServiceOrders;
+import org.etsi.osl.tmf.metrics.ServiceOrdersGroupByDay;
+import org.etsi.osl.tmf.metrics.ServiceOrdersGroupByState;
+import org.etsi.osl.tmf.metrics.TotalServiceOrders;
+import org.etsi.osl.tmf.so641.model.ServiceOrderStateType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.format.annotation.DateTimeFormat;
+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.RequestParam;
+
+import java.time.OffsetDateTime;
+import java.util.Map;
+
+@Tag(name = "ServiceOrderMetricsApi", description = "The Service Orders' Metrics API")
+public interface ServiceOrderMetricsApi {
+
+ Logger log = LoggerFactory.getLogger(ServiceOrderMetricsApi.class);
+
+ @Operation(summary = "Get total number of service orders", operationId = "getTotalServiceOrders")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Success"),
+ @ApiResponse(responseCode = "400", description = "Bad Request"),
+ @ApiResponse(responseCode = "500", description = "Internal Server Error")
+ })
+ @RequestMapping(value = "/metrics/totalServiceOrders", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
+ ResponseEntity getTotalServiceOrders(
+ @Valid @RequestParam(value = "state", required = false) ServiceOrderStateType state
+ );
+
+
+ @Operation(summary = "Get total number of active service orders", operationId = "getTotalActiveServiceOrders")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Success"),
+ @ApiResponse(responseCode = "400", description = "Bad Request"),
+ @ApiResponse(responseCode = "500", description = "Internal Server Error")
+ })
+ @RequestMapping(value = "/metrics/activeServiceOrders", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
+ ResponseEntity getTotalActiveServiceOrders();
+
+
+ @Operation(summary = "Get service orders grouped by day", operationId = "getServiceOrdersGroupedByDay")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Success"),
+ @ApiResponse(responseCode = "400", description = "Bad Request"),
+ @ApiResponse(responseCode = "500", description = "Internal Server Error")
+ })
+ @RequestMapping(value = "/metrics/serviceOrdersGroupByDay", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
+ ResponseEntity getServiceOrdersGroupedByDay(
+ @Valid @RequestParam(value = "starttime", required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime starttime,
+ @Valid @RequestParam(value = "endtime", required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime endtime
+ );
+
+
+ @Operation(summary = "Get service orders grouped by state", operationId = "getServiceOrdersGroupedByState")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Success"),
+ @ApiResponse(responseCode = "400", description = "Bad Request"),
+ @ApiResponse(responseCode = "500", description = "Internal Server Error")
+ })
+ @RequestMapping(value = "/metrics/serviceOrdersGroupByState", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
+ ResponseEntity getServiceOrdersGroupedByState(
+ @Valid @RequestParam(value = "starttime", required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime starttime,
+ @Valid @RequestParam(value = "endtime", required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime endtime
+ );
+}
diff --git a/src/main/java/org/etsi/osl/tmf/metrics/api/ServiceOrderMetricsApiController.java b/src/main/java/org/etsi/osl/tmf/metrics/api/ServiceOrderMetricsApiController.java
new file mode 100644
index 0000000000000000000000000000000000000000..6b0a1bf8efe15abe954d20c65fd20f24a65603e8
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/metrics/api/ServiceOrderMetricsApiController.java
@@ -0,0 +1,123 @@
+package org.etsi.osl.tmf.metrics.api;
+
+import org.etsi.osl.tmf.metrics.*;
+import org.etsi.osl.tmf.metrics.reposervices.ServiceOrderMetricsRepoService;
+import org.etsi.osl.tmf.so641.model.ServiceOrderStateType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+
+import java.time.OffsetDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+
+@Controller
+public class ServiceOrderMetricsApiController implements ServiceOrderMetricsApi {
+
+ private static final Logger log = LoggerFactory.getLogger(ServiceOrderMetricsApiController.class);
+ private final ServiceOrderMetricsRepoService serviceOrderMetricsRepoService;
+
+ @Autowired
+ public ServiceOrderMetricsApiController(ServiceOrderMetricsRepoService serviceOrderMetricsRepoService) {
+ this.serviceOrderMetricsRepoService = serviceOrderMetricsRepoService;
+ }
+
+ @Override
+ public ResponseEntity getTotalServiceOrders(ServiceOrderStateType state) {
+ try {
+ int totalServiceOrders = serviceOrderMetricsRepoService.countTotalServiceOrders(state);
+ TotalServiceOrders response = new TotalServiceOrders(totalServiceOrders);
+ return new ResponseEntity<>(response, HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Couldn't retrieve total service orders. ", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity getTotalActiveServiceOrders() {
+ try {
+ int totalActiveServiceOrders = serviceOrderMetricsRepoService.countTotalActiveServiceOrders();
+ ActiveServiceOrders response = new ActiveServiceOrders(totalActiveServiceOrders);
+ return new ResponseEntity<>(response, HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Couldn't retrieve total active service orders. ", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity getServiceOrdersGroupedByDay(OffsetDateTime starttime, OffsetDateTime endtime) {
+ try {
+ Map orderDatesGroupedByDate = serviceOrderMetricsRepoService.getServiceOrdersGroupedByDay(starttime, endtime);
+
+ // Fill missing days with count 0
+ Map fullDayMap = new LinkedHashMap<>();
+ OffsetDateTime cursor = starttime.truncatedTo(ChronoUnit.DAYS);
+ OffsetDateTime endDay = endtime.truncatedTo(ChronoUnit.DAYS);
+ while (!cursor.isAfter(endDay)) {
+ String key = cursor.toInstant().toString();
+ fullDayMap.put(key, orderDatesGroupedByDate.getOrDefault(key, 0));
+ cursor = cursor.plusDays(1);
+ }
+
+ // Convert to model list
+ List groupByDayList = fullDayMap.entrySet().stream()
+ .map(entry -> new ServiceOrdersGroupByDayItem(entry.getKey(), entry.getValue()))
+ .toList();
+
+ ServiceOrdersGroupByDayAggregations aggregations = new ServiceOrdersGroupByDayAggregations(groupByDayList);
+ int total = fullDayMap.values().stream().mapToInt(Integer::intValue).sum();
+ ServiceOrdersGroupByDayParent wrapper = new ServiceOrdersGroupByDayParent(total, aggregations);
+ ServiceOrdersGroupByDay response = new ServiceOrdersGroupByDay(wrapper);
+
+ return new ResponseEntity<>(response, HttpStatus.OK);
+
+ } catch (Exception e) {
+ log.error("Couldn't retrieve services grouped by state. ", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity getServiceOrdersGroupedByState(OffsetDateTime starttime, OffsetDateTime endtime) {
+ try {
+ Map servicesByState = serviceOrderMetricsRepoService.getServiceOrdersGroupedByState(starttime, endtime);
+
+ // Initialize with all possible states and 0. Ensures that all states are represented, even if not present in the data.
+ Map fullStateMap = new LinkedHashMap<>();
+ for (ServiceOrderStateType state : ServiceOrderStateType.values()) {
+ fullStateMap.put(state.name(), 0);
+ }
+
+ // Overwrite counts with actual data
+ servicesByState.forEach((key, value) -> {
+ fullStateMap.put(key.toUpperCase(), value);
+ });
+
+ // Create aggregation items
+ List groupByStateList = fullStateMap.entrySet().stream()
+ .map(entry -> new ServiceOrdersGroupByStateItem(ServiceOrderStateType.valueOf(entry.getKey()), entry.getValue()))
+ .toList();
+
+ // Build response structure using models
+ ServiceOrdersGroupByStateAggregations aggregations = new ServiceOrdersGroupByStateAggregations(groupByStateList);
+ int total = fullStateMap.values().stream().mapToInt(Integer::intValue).sum();
+ ServiceOrdersGroupByStateParent services = new ServiceOrdersGroupByStateParent(total, aggregations);
+ ServiceOrdersGroupByState response = new ServiceOrdersGroupByState(services);
+
+ return new ResponseEntity<>(response, HttpStatus.OK);
+
+ } catch (Exception e) {
+ log.error("Couldn't retrieve services grouped by state. ", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+}
diff --git a/src/main/java/org/etsi/osl/tmf/metrics/reposervices/GeneralMetricsRepoService.java b/src/main/java/org/etsi/osl/tmf/metrics/reposervices/GeneralMetricsRepoService.java
new file mode 100644
index 0000000000000000000000000000000000000000..fb3af6a76e120fe77896d12a2d50178d7c5cfb34
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/metrics/reposervices/GeneralMetricsRepoService.java
@@ -0,0 +1,74 @@
+package org.etsi.osl.tmf.metrics.reposervices;
+
+import org.etsi.osl.tmf.pm632.repo.IndividualRepository;
+import org.etsi.osl.tmf.rcm634.repo.ResourceSpecificationRepository;
+import org.etsi.osl.tmf.scm633.model.ServiceCatalog;
+import org.etsi.osl.tmf.scm633.model.ServiceCategory;
+import org.etsi.osl.tmf.scm633.repo.CandidateRepository;
+import org.etsi.osl.tmf.scm633.repo.CatalogRepository;
+import org.etsi.osl.tmf.scm633.repo.CategoriesRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Service
+public class GeneralMetricsRepoService {
+
+ @Autowired
+ IndividualRepository individualRepository;
+
+ @Autowired
+ ResourceSpecificationRepository resourceSpecificationRepository;
+
+ @Autowired
+ CatalogRepository catalogRepository;
+
+ @Autowired
+ CategoriesRepository categoriesRepository;
+
+ @Autowired
+ CandidateRepository candidateRepository;
+
+ // Cached values for performance optimization
+ private static Integer cachedPublishedServiceSpecCount = null;
+ private static OffsetDateTime lastRetrieved = null;
+ private static final Duration CACHE_DURATION = Duration.ofMinutes(15);
+
+ public int countRegisteredIndividuals() {
+ return individualRepository.countAll();
+ }
+
+ public int countPublishedServiceSpecifications() {
+ OffsetDateTime now = OffsetDateTime.now();
+
+ if (cachedPublishedServiceSpecCount == null || lastRetrieved == null ||
+ Duration.between(lastRetrieved, now).compareTo(CACHE_DURATION) > 0) {
+
+ int count = 0;
+ Set serviceCategories = new HashSet<>();
+ List serviceCatalogs = catalogRepository.findByOrderByName();
+
+ for (ServiceCatalog serviceCatalog: serviceCatalogs) {
+ serviceCategories.addAll(serviceCatalog.getCategoryObj());
+ }
+
+ for (ServiceCategory serviceCategory : serviceCategories) {
+ count += serviceCategory.getServiceCandidateObj().size()
+ + serviceCategory.getServiceCandidateRefs().size();
+ }
+
+ cachedPublishedServiceSpecCount = count;
+ lastRetrieved = now;
+ }
+ return cachedPublishedServiceSpecCount;
+ }
+
+ public int countRegisteredResourceSpecifications() {
+ return resourceSpecificationRepository.countLogical() + resourceSpecificationRepository.countPhysical();
+ }
+}
diff --git a/src/main/java/org/etsi/osl/tmf/metrics/reposervices/ResourceMetricsRepoService.java b/src/main/java/org/etsi/osl/tmf/metrics/reposervices/ResourceMetricsRepoService.java
new file mode 100644
index 0000000000000000000000000000000000000000..02d26d518a6828de96b3cd36415aba2c75dac6e3
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/metrics/reposervices/ResourceMetricsRepoService.java
@@ -0,0 +1,47 @@
+package org.etsi.osl.tmf.metrics.reposervices;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.etsi.osl.tmf.ri639.model.ResourceStatusType;
+import org.etsi.osl.tmf.ri639.repo.ResourceRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+
+@Service
+public class ResourceMetricsRepoService {
+
+ @Autowired
+ ObjectMapper objectMapper;
+
+ @Autowired
+ ResourceRepository resourceRepository;
+
+ public int countTotalResources(ResourceStatusType state) {
+ if (state == null) {
+ return resourceRepository.countAll();
+ } else {
+ return resourceRepository.countByResourceStatus(state);
+ }
+ }
+
+ public Map getResourcesGroupedByState(OffsetDateTime starttime, OffsetDateTime endtime) {
+ if (starttime.plusDays(31).isBefore(endtime)) {
+ starttime = endtime.minusDays(31);
+ }
+
+ List