Commit 72b84e9c authored by Nikolaos Kyriakoulis's avatar Nikolaos Kyriakoulis
Browse files

Created metrics endpoints for TMP Resource related information

parent c6a2420c
Loading
Loading
Loading
Loading
Loading
+47 −0
Original line number Diff line number Diff line
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.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")
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 = "/tmf-api/metrics/totalResources", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
    ResponseEntity<Map<String, Integer>> 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 = "/tmf-api/metrics/resourcesGroupByState", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
    ResponseEntity<Map<String, Object>> 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
    );
}
+85 −0
Original line number Diff line number Diff line
package org.etsi.osl.tmf.metrics.api;

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.HashMap;
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<Map<String, Integer>> getTotalResources(ResourceStatusType state) {
        try {
            int totalResources = resourceMetricsRepoService.countTotalResources(state);
            Map<String, Integer> response = new HashMap<>();
            response.put("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<Map<String, Object>> getResourcesGroupedByState(OffsetDateTime starttime, OffsetDateTime endtime) {
        try {
            Map<String, Integer> 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<String, Integer> 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
            });

            // Build groupByState list
            List<Map<String, Object>> groupByStateList = fullStateMap.entrySet().stream()
                    .map(entry -> {
                        Map<String, Object> map = new HashMap<>();
                        map.put("key", entry.getKey());
                        map.put("count", entry.getValue());
                        return map;
                    })
                    .toList();


            // Wrap in response structure
            Map<String, Object> aggregations = Map.of("groupByState", groupByStateList);
            Map<String, Object> services = Map.of(
                    "total", fullStateMap.values().stream().mapToInt(Integer::intValue).sum(),
                    "aggregations", aggregations
            );

            Map<String, Object> response = Map.of("resources", 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);
        }
    }

}
+46 −0
Original line number Diff line number Diff line
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<String, Integer> getResourcesGroupedByState(OffsetDateTime starttime, OffsetDateTime endtime) {
        if (starttime.plusDays(31).isBefore(endtime)) {
            starttime = endtime.minusDays(31);
        }

        List<Object[]> rawResults = resourceRepository.groupByStateBetweenDates(starttime, endtime);

        return rawResults.stream()
                .collect(Collectors.toMap(
                        row -> row[0].toString(),
                        row -> ((Number) row[1]).intValue()
                ));
    }

}
+16 −0
Original line number Diff line number Diff line
@@ -19,9 +19,11 @@
 */
package org.etsi.osl.tmf.ri639.repo;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import org.etsi.osl.tmf.ri639.model.Resource;
import org.etsi.osl.tmf.ri639.model.ResourceStatusType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@@ -56,4 +58,18 @@ public interface ResourceRepository extends JpaRepository<Resource, Long> {

	List<Resource> findByNameAndResourceVersion(String aname, String aversion);
	List<Resource> findByNameAndCategoryAndResourceVersion(String aname, String acategory, String aversion);


	// Methods for metrics

	@Query("SELECT COUNT(res) FROM RIResource res")
	int countAll();

	int countByResourceStatus(ResourceStatusType status);

	@Query("SELECT res.resourceStatus, COUNT(res) FROM RIResource res "
			+ "WHERE res.startOperatingDate >= :starttime AND res.endOperatingDate <= :endtime "
			+ "GROUP BY res.resourceStatus")
	List<Object[]> groupByStateBetweenDates(OffsetDateTime starttime, OffsetDateTime endtime);

}
+240 −0
Original line number Diff line number Diff line
package org.etsi.osl.services.api.metrics;

import com.jayway.jsonpath.JsonPath;
import org.apache.commons.io.IOUtils;
import org.etsi.osl.tmf.JsonUtils;
import org.etsi.osl.tmf.OpenAPISpringBoot;
import org.etsi.osl.tmf.common.model.Any;
import org.etsi.osl.tmf.common.model.UserPartRoleType;
import org.etsi.osl.tmf.common.model.service.Note;
import org.etsi.osl.tmf.common.model.service.ResourceRef;
import org.etsi.osl.tmf.prm669.model.RelatedParty;
import org.etsi.osl.tmf.rcm634.model.*;
import org.etsi.osl.tmf.ri639.model.*;
import org.etsi.osl.tmf.ri639.reposervices.ResourceRepoService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URI;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@Transactional
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.MOCK,
        classes = OpenAPISpringBoot.class
)
//@AutoConfigureTestDatabase //this automatically uses h2
@AutoConfigureMockMvc
@ActiveProfiles("testing")
//@TestPropertySource(
//		  locations = "classpath:application-testing.yml")
public class ResourceMetricsApiControllerTest {

    @Autowired
    private MockMvc mvc;

    @Autowired
    ResourceRepoService resourceRepoService;

    @Autowired
    private WebApplicationContext context;

    @Before
    public void setup() throws Exception {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build();
    }

    @WithMockUser(username="osadmin", roles = {"ADMIN","USER"})
    @Test
    public void testCountTotalResources() throws Exception {
        createResource(ResourceStatusType.AVAILABLE);

        String response = mvc.perform(MockMvcRequestBuilders.get("/tmf-api/metrics/totalResources" )
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk() )
                .andReturn().getResponse().getContentAsString();

        int totalResources = JsonPath.read(response, "$.totalResources");


        assertThat(totalResources).isEqualTo(resourceRepoService.findAll().size());
    }

    @WithMockUser(username="osadmin", roles = {"ADMIN","USER"})
    @Test
    public void testCountTotalServicesWithState() throws Exception {
        createResource(ResourceStatusType.AVAILABLE);
        createResource(ResourceStatusType.STANDBY);

        String response = mvc.perform(MockMvcRequestBuilders.get("/tmf-api/metrics/totalResources" )
                        .param("state", "STANDBY")
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk() )
                .andReturn().getResponse().getContentAsString();

        int totalResources = JsonPath.read(response, "$.totalResources");


        List<Resource> resourcesList = resourceRepoService.findAll();
        int standByResources = (int) resourcesList.stream().filter(resource -> resource.getResourceStatus() == ResourceStatusType.STANDBY).count();

        assertThat(totalResources).isEqualTo(standByResources);
        assertThat(totalResources).isEqualTo(1);
    }

    @WithMockUser(username = "osadmin", roles = {"ADMIN", "USER"})
    @Test
    public void testGetServicesGroupedByState() throws Exception {
        String startTime = OffsetDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);

        createResource(ResourceStatusType.AVAILABLE);
        createResource(ResourceStatusType.AVAILABLE);
        createResource(ResourceStatusType.AVAILABLE);
        createResource(ResourceStatusType.RESERVED);
        createResource(ResourceStatusType.RESERVED);
        createResource(ResourceStatusType.STANDBY);

        String endTime = OffsetDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);

        String response = mvc.perform(MockMvcRequestBuilders.get("/tmf-api/metrics/resourcesGroupByState")
                        .param("starttime", startTime)
                        .param("endtime", endTime)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn().getResponse().getContentAsString();

        List<Map<String, Object>> groupByState = JsonPath.read(response, "$.resources.aggregations.groupByState");

        // Create a map from key -> count
        Map<String, Integer> stateCounts = groupByState.stream()
                .collect(Collectors.toMap(
                        entry -> (String) entry.get("key"),
                        entry -> (Integer) entry.get("count")
                ));

        assertThat(stateCounts.get("AVAILABLE")).isEqualTo(3);
        assertThat(stateCounts.get("RESERVED")).isEqualTo(2);
        assertThat(stateCounts.get("STANDBY")).isEqualTo(1);
        assertThat(stateCounts.get("ALARM")).isEqualTo(0);
        assertThat(stateCounts.get("UNKNOWN")).isEqualTo(0);
        assertThat(stateCounts.get("SUSPENDED")).isEqualTo(0);
    }

    private void createResource(ResourceStatusType statusType) throws Exception{

        /**
         * first add 2 specs
         */

        File sspec = new File( "src/test/resources/testResourceSpec.json" );
        InputStream in = new FileInputStream( sspec );
        String sspectext = IOUtils.toString(in, "UTF-8");


        ResourceSpecificationCreate sspeccr1 = JsonUtils.toJsonObj( sspectext,  ResourceSpecificationCreate.class);
        sspeccr1.setName("Spec1");
        ResourceSpecification responsesSpec1 = createResourceSpec( sspeccr1);

        //res 2 is an RFS
        ResourceSpecificationCreate sspeccr2 = JsonUtils.toJsonObj( sspectext,  ResourceSpecificationCreate.class);
        sspeccr2.setName("Spec2");

        sspeccr2.addResourceSpecificationRelationshipWith( responsesSpec1 );
        LogicalResourceSpecification responsesSpec2 = (LogicalResourceSpecification) createResourceSpec( sspeccr2 );
        /**
         * add them as bundle
         */

        ResourceSpecificationCreate sspeccr3 = JsonUtils.toJsonObj( sspectext,  ResourceSpecificationCreate.class);
        sspeccr3.setName("BundleExampleSpec");
        sspeccr3.isBundle(true);
        sspeccr3.addResourceSpecificationRelationshipWith( responsesSpec1 );
        sspeccr3.addResourceSpecificationRelationshipWith( responsesSpec2 );
        ResourceSpecification responsesSpec3 = createResourceSpec( sspeccr3);

        ResourceCreate aResource = new ResourceCreate();
        aResource.setName("aNew Resource parent");
        aResource.setCategory("Experimentation");
        aResource.setDescription("Experimentation Descr");
        aResource.setStartOperatingDate( OffsetDateTime.now(ZoneOffset.UTC ).toString() );
        aResource.setEndOperatingDate( OffsetDateTime.now(ZoneOffset.UTC ).toString() );
        aResource.setResourceStatus(statusType);



        Note noteItem = new Note();
        noteItem.text("test note");
        aResource.addNoteItem(noteItem);

        Characteristic resCharacteristicItem = new Characteristic();

        resCharacteristicItem.setName( "externalPartnerServiceId" );
        resCharacteristicItem.setValue( new Any("NONE"));
        aResource.addResourceCharacteristicItem(resCharacteristicItem);

        ResourceSpecificationRef aServiceSpecificationRef = new ResourceSpecificationRef();
        aServiceSpecificationRef.setId(responsesSpec3.getId() );
        aServiceSpecificationRef.setName(responsesSpec3.getName());

        aResource.setResourceSpecification( aServiceSpecificationRef );

        resourceRepoService.addResource(aResource);
    }


    private ResourceSpecification createResourceSpec(ResourceSpecificationUpdate sspeccr1) throws Exception{

        URI url = new URI("/resourceCatalogManagement/v4/resourceSpecification");
        if (sspeccr1 instanceof PhysicalResourceSpecificationUpdate ) {
            url = new URI("/resourceCatalogManagement/v4/resourceSpecification");
        }
        String responseSpec = mvc.perform(MockMvcRequestBuilders.post( url  )
                        .with( SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content( JsonUtils.toJson( sspeccr1 ) ))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn().getResponse().getContentAsString();

        ResourceSpecification responsesSpec1;
        if (sspeccr1 instanceof PhysicalResourceSpecificationUpdate ) {
            responsesSpec1 = JsonUtils.toJsonObj(responseSpec,  PhysicalResourceSpecification.class);
        }else {
            responsesSpec1 = JsonUtils.toJsonObj(responseSpec,  LogicalResourceSpecification.class);
        }

        return responsesSpec1;
    }
}