diff --git a/kraken-app/kraken-app-agent/src/main/resources/application.yaml b/kraken-app/kraken-app-agent/src/main/resources/application.yaml index dc35340b..f4e480d4 100644 --- a/kraken-app/kraken-app-agent/src/main/resources/application.yaml +++ b/kraken-app/kraken-app-agent/src/main/resources/application.yaml @@ -92,6 +92,9 @@ app: controlPlane: url: http://localhost:8001 token: Bearer ${KRAKEN_MGMT_API_ACCESS_TOKEN:FAKE_TOKEN} + push-activity-log-external: + enabled: false + batch-size: ${EXTERNAL_SYSTEM_PUSH_LOG_BATCH_SIZE:200} cron-job: pull-api-server-info: 0/20 * * * * * pull-server-assets: 0 0/1 * * * * @@ -99,6 +102,7 @@ app: pull-reset-event: 0 0/1 * * * * push-heartbeat: 0/20 * * * * * push-log: 0/20 * * * * * + push-log-external-system: 0 0/1 * * * * accept-asset-kinds: - kraken.component.api - kraken.component.api-target @@ -107,4 +111,4 @@ app: unified-asset: endpoints: exposure: - include: asset + include: asset \ No newline at end of file diff --git a/kraken-app/kraken-app-controller/src/main/resources/application.yaml b/kraken-app/kraken-app-controller/src/main/resources/application.yaml index 5c83f10c..f9ecc330 100644 --- a/kraken-app/kraken-app-controller/src/main/resources/application.yaml +++ b/kraken-app/kraken-app-controller/src/main/resources/application.yaml @@ -193,6 +193,9 @@ app: endpoints: exposure: include: asset, component, product, ingestion, component-operation, component-mapper + features: + push-activity-log-external: + enabled: false initialize-exclude-assets: - classpath:/mef-sonata/apis/api.product.offering.yaml diff --git a/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/api/APIActivityPushLogController.java b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/api/APIActivityPushLogController.java new file mode 100644 index 00000000..14b2a663 --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/api/APIActivityPushLogController.java @@ -0,0 +1,74 @@ +package com.consoleconnect.kraken.operator.controller.api; + +import static com.consoleconnect.kraken.operator.core.service.UnifiedAssetService.getSearchPageRequest; + +import com.consoleconnect.kraken.operator.auth.security.UserContext; +import com.consoleconnect.kraken.operator.controller.dto.push.ApiRequestActivityPushResult; +import com.consoleconnect.kraken.operator.controller.dto.push.CreatePushApiActivityRequest; +import com.consoleconnect.kraken.operator.controller.dto.push.PushApiActivityLogEnabled; +import com.consoleconnect.kraken.operator.controller.dto.push.PushApiActivityLogHistory; +import com.consoleconnect.kraken.operator.controller.service.push.ApiActivityPushService; +import com.consoleconnect.kraken.operator.core.model.HttpResponse; +import com.consoleconnect.kraken.operator.core.request.PushLogSearchRequest; +import com.consoleconnect.kraken.operator.core.toolkit.Paging; +import com.consoleconnect.kraken.operator.core.toolkit.PagingHelper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.time.ZonedDateTime; +import lombok.AllArgsConstructor; +import org.springframework.data.domain.Sort; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +@AllArgsConstructor +@RestController() +@RequestMapping(value = "/push-api-activity-log", produces = MediaType.APPLICATION_JSON_VALUE) +@Tag(name = "API Activities Push Logs", description = "API Activities Push Logs") +public class APIActivityPushLogController { + + private final ApiActivityPushService apiActivityPushService; + + @Operation(summary = "Store api activity log info") + @PostMapping + public Mono> createPushApiActivityLogInfo( + @RequestBody CreatePushApiActivityRequest request) { + return UserContext.getUserId() + .publishOn(Schedulers.boundedElastic()) + .map(userId -> apiActivityPushService.createPushApiActivityLogInfo(request, userId)) + .map(HttpResponse::ok); + } + + @Operation(summary = "search push history") + @GetMapping("/history") + public HttpResponse> searchHistory( + @RequestParam(value = "orderBy", required = false, defaultValue = "createdAt") String orderBy, + @RequestParam(value = "direction", required = false, defaultValue = "DESC") + Sort.Direction direction, + @RequestParam(value = "page", required = false, defaultValue = PagingHelper.DEFAULT_PAGE_STR) + int page, + @RequestParam(value = "size", required = false, defaultValue = PagingHelper.DEFAULT_SIZE_STR) + int size, + @RequestParam(value = "requestStartTime", required = false) ZonedDateTime requestStartTime, + @RequestParam(value = "requestEndTime", required = false) ZonedDateTime requestEndTime) { + return HttpResponse.ok( + this.apiActivityPushService.searchHistory( + PushLogSearchRequest.builder() + .queryStart(requestStartTime) + .queryEnd(requestEndTime) + .build(), + getSearchPageRequest(page, size, direction, orderBy))); + } + + @Operation(summary = "check if push log enabled") + @GetMapping("/enabled") + public HttpResponse isPushApiActivityLogEnabled() { + return HttpResponse.ok(this.apiActivityPushService.isPushApiActivityLogEnabled()); + } +} diff --git a/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/dto/push/ApiRequestActivityPushResult.java b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/dto/push/ApiRequestActivityPushResult.java new file mode 100644 index 00000000..c3414cc0 --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/dto/push/ApiRequestActivityPushResult.java @@ -0,0 +1,13 @@ +package com.consoleconnect.kraken.operator.controller.dto.push; + +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ApiRequestActivityPushResult { + private UUID id; +} diff --git a/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/dto/push/CreatePushApiActivityRequest.java b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/dto/push/CreatePushApiActivityRequest.java new file mode 100644 index 00000000..78147b1c --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/dto/push/CreatePushApiActivityRequest.java @@ -0,0 +1,15 @@ +package com.consoleconnect.kraken.operator.controller.dto.push; + +import java.time.ZonedDateTime; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreatePushApiActivityRequest { + private ZonedDateTime startTime; + private ZonedDateTime endTime; + private String envId; +} diff --git a/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/dto/push/PushApiActivityLogEnabled.java b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/dto/push/PushApiActivityLogEnabled.java new file mode 100644 index 00000000..83f7339f --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/dto/push/PushApiActivityLogEnabled.java @@ -0,0 +1,12 @@ +package com.consoleconnect.kraken.operator.controller.dto.push; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PushApiActivityLogEnabled { + private boolean enabled; +} diff --git a/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/dto/push/PushApiActivityLogHistory.java b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/dto/push/PushApiActivityLogHistory.java new file mode 100644 index 00000000..c54d7cdb --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/dto/push/PushApiActivityLogHistory.java @@ -0,0 +1,20 @@ +package com.consoleconnect.kraken.operator.controller.dto.push; + +import java.time.ZonedDateTime; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PushApiActivityLogHistory { + private UUID id; + private ZonedDateTime createdAt; + private String envName; + private ZonedDateTime startTime; + private ZonedDateTime endTime; + private String pushedBy; + private String status; +} diff --git a/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/service/push/ApiActivityPushService.java b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/service/push/ApiActivityPushService.java new file mode 100644 index 00000000..c5c1712d --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/service/push/ApiActivityPushService.java @@ -0,0 +1,144 @@ +package com.consoleconnect.kraken.operator.controller.service.push; + +import static com.consoleconnect.kraken.operator.core.toolkit.JsonToolkit.fromJson; +import static com.consoleconnect.kraken.operator.core.toolkit.JsonToolkit.toJson; + +import com.consoleconnect.kraken.operator.controller.dto.push.ApiRequestActivityPushResult; +import com.consoleconnect.kraken.operator.controller.dto.push.CreatePushApiActivityRequest; +import com.consoleconnect.kraken.operator.controller.dto.push.PushApiActivityLogEnabled; +import com.consoleconnect.kraken.operator.controller.dto.push.PushApiActivityLogHistory; +import com.consoleconnect.kraken.operator.controller.model.Environment; +import com.consoleconnect.kraken.operator.controller.service.EnvironmentService; +import com.consoleconnect.kraken.operator.core.entity.MgmtEventEntity; +import com.consoleconnect.kraken.operator.core.enums.EventStatusType; +import com.consoleconnect.kraken.operator.core.enums.MgmtEventType; +import com.consoleconnect.kraken.operator.core.exception.KrakenException; +import com.consoleconnect.kraken.operator.core.model.AppProperty; +import com.consoleconnect.kraken.operator.core.repo.MgmtEventRepository; +import com.consoleconnect.kraken.operator.core.request.PushLogSearchRequest; +import com.consoleconnect.kraken.operator.core.toolkit.Paging; +import com.consoleconnect.kraken.operator.core.toolkit.PagingHelper; +import jakarta.persistence.criteria.Predicate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class ApiActivityPushService { + + public static final String CREATED_AT = "createdAt"; + public static final String EVENT_TYPE = "eventType"; + public static final String PUSH_API_ACTIVITY_LOGS_IS_DISABLED = + "Push api activity logs is disabled."; + public static final String THE_SAME_PARAMETERS_ALREADY_EXISTS_ERROR = + "Push event with the same parameters already exists with status 'ack' or 'in_progress'."; + + private final MgmtEventRepository mgmtEventRepository; + private final EnvironmentService environmentService; + private final AppProperty appProperty; + + public ApiRequestActivityPushResult createPushApiActivityLogInfo( + CreatePushApiActivityRequest request, String userId) { + validateRequest(request); + var environment = environmentService.findOne(request.getEnvId()); + var entity = new MgmtEventEntity(); + entity.setStatus(EventStatusType.ACK.name()); + entity.setEventType(MgmtEventType.PUSH_API_ACTIVITY_LOG.name()); + entity.setPayload(toJson(createData(request, userId, environment))); + var saved = mgmtEventRepository.save(entity); + return new ApiRequestActivityPushResult(saved.getId()); + } + + private void validateRequest(CreatePushApiActivityRequest searchRequest) { + validateIfFeatureIsEnabled(); + validatePushEventWithTheSameParameters(searchRequest); + } + + private void validateIfFeatureIsEnabled() { + if (!appProperty.getFeatures().getPushActivityLogExternal().isEnabled()) { + throw new KrakenException(400, PUSH_API_ACTIVITY_LOGS_IS_DISABLED); + } + } + + private void validatePushEventWithTheSameParameters(CreatePushApiActivityRequest searchRequest) { + boolean exists = + mgmtEventRepository.existsBy( + List.of(EventStatusType.ACK.name(), EventStatusType.IN_PROGRESS.name()), + searchRequest.getEnvId(), + toUtcString(searchRequest.getStartTime()), + toUtcString(searchRequest.getEndTime()), + MgmtEventType.PUSH_API_ACTIVITY_LOG.name()); + + if (exists) { + throw new KrakenException(400, THE_SAME_PARAMETERS_ALREADY_EXISTS_ERROR); + } + } + + private String toUtcString(ZonedDateTime zonedDateTime) { + return zonedDateTime + .withZoneSameInstant(ZoneOffset.UTC) + .format(DateTimeFormatter.ISO_ZONED_DATE_TIME); + } + + private PushLogActivityLogInfo createData( + CreatePushApiActivityRequest request, String userId, Environment environment) { + var data = new PushLogActivityLogInfo(); + data.setUser(userId); + data.setEnvId(request.getEnvId()); + data.setEnvName(environment.getName()); + data.setStartTime(toUtcString(request.getStartTime())); + data.setEndTime(toUtcString(request.getEndTime())); + return data; + } + + public Paging searchHistory( + PushLogSearchRequest searchRequest, PageRequest pageRequest) { + Page pushEvents = + mgmtEventRepository.findAll(getMgmtEventEntitySpecification(searchRequest), pageRequest); + return PagingHelper.toPaging( + pushEvents, + e -> { + var payload = fromJson(e.getPayload(), PushLogActivityLogInfo.class); + return new PushApiActivityLogHistory( + e.getId(), + e.getCreatedAt(), + payload.getEnvName(), + ZonedDateTime.parse(payload.getStartTime()), + ZonedDateTime.parse(payload.getEndTime()), + payload.getUser(), + e.getStatus()); + }); + } + + private static Specification getMgmtEventEntitySpecification( + PushLogSearchRequest searchRequest) { + return (root, query, criteriaBuilder) -> { + var predicateList = new ArrayList(); + predicateList.add( + criteriaBuilder.equal(root.get(EVENT_TYPE), MgmtEventType.PUSH_API_ACTIVITY_LOG.name())); + if (searchRequest.getQueryStart() != null) { + predicateList.add( + criteriaBuilder.greaterThanOrEqualTo( + root.get(CREATED_AT), searchRequest.getQueryStart())); + } + if (searchRequest.getQueryEnd() != null) { + predicateList.add( + criteriaBuilder.lessThanOrEqualTo(root.get(CREATED_AT), searchRequest.getQueryEnd())); + } + return query.where(predicateList.toArray(new Predicate[0])).getRestriction(); + }; + } + + public PushApiActivityLogEnabled isPushApiActivityLogEnabled() { + return new PushApiActivityLogEnabled( + appProperty.getFeatures().getPushActivityLogExternal().isEnabled()); + } +} diff --git a/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/service/push/PushLogActivityLogInfo.java b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/service/push/PushLogActivityLogInfo.java new file mode 100644 index 00000000..08924312 --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-controller/src/main/java/com/consoleconnect/kraken/operator/controller/service/push/PushLogActivityLogInfo.java @@ -0,0 +1,12 @@ +package com.consoleconnect.kraken.operator.controller.service.push; + +import lombok.Data; + +@Data +public class PushLogActivityLogInfo { + private String user; + private String startTime; + private String envId; + private String envName; + private String endTime; +} diff --git a/kraken-java-sdk/kraken-java-sdk-controller/src/test/java/com/consoleconnect/kraken/operator/controller/api/APIActivityPushLogControllerTest.java b/kraken-java-sdk/kraken-java-sdk-controller/src/test/java/com/consoleconnect/kraken/operator/controller/api/APIActivityPushLogControllerTest.java new file mode 100644 index 00000000..557a9157 --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-controller/src/test/java/com/consoleconnect/kraken/operator/controller/api/APIActivityPushLogControllerTest.java @@ -0,0 +1,120 @@ +package com.consoleconnect.kraken.operator.controller.api; + +import static com.consoleconnect.kraken.operator.core.service.UnifiedAssetService.getSearchPageRequest; +import static java.time.ZonedDateTime.parse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.consoleconnect.kraken.operator.config.TestApplication; +import com.consoleconnect.kraken.operator.controller.WebTestClientHelper; +import com.consoleconnect.kraken.operator.controller.dto.push.ApiRequestActivityPushResult; +import com.consoleconnect.kraken.operator.controller.dto.push.CreatePushApiActivityRequest; +import com.consoleconnect.kraken.operator.controller.dto.push.PushApiActivityLogEnabled; +import com.consoleconnect.kraken.operator.controller.dto.push.PushApiActivityLogHistory; +import com.consoleconnect.kraken.operator.controller.service.push.ApiActivityPushService; +import com.consoleconnect.kraken.operator.core.model.HttpResponse; +import com.consoleconnect.kraken.operator.core.request.PushLogSearchRequest; +import com.consoleconnect.kraken.operator.core.toolkit.Paging; +import com.consoleconnect.kraken.operator.core.toolkit.PagingHelper; +import com.consoleconnect.kraken.operator.test.AbstractIntegrationTest; +import com.consoleconnect.kraken.operator.test.MockIntegrationTest; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.ZoneOffset; +import java.util.List; +import java.util.UUID; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.reactive.server.WebTestClient; + +@ActiveProfiles("test-auth-server-enabled") +@MockIntegrationTest +@ContextConfiguration(classes = TestApplication.class) +class APIActivityPushLogControllerTest extends AbstractIntegrationTest { + + @MockBean private ApiActivityPushService service; + @Autowired private ObjectMapper objectMapper; + private final WebTestClientHelper testClientHelper; + + @Autowired + APIActivityPushLogControllerTest(WebTestClient webTestClient) { + testClientHelper = new WebTestClientHelper(webTestClient); + } + + @Test + void givenApiActivityLogs_whenCreatingApiLogsPushRequest_thenReturnsOk() { + // given + var envId = UUID.randomUUID(); + var endTime = parse("2024-10-10T00:00:00+01:00"); + var startTime = endTime.minusDays(1); + var request = new CreatePushApiActivityRequest(startTime, endTime, envId.toString()); + + var pushResult = new ApiRequestActivityPushResult(UUID.randomUUID()); + when(service.createPushApiActivityLogInfo( + new CreatePushApiActivityRequest( + startTime.withZoneSameInstant(ZoneOffset.UTC), + endTime.withZoneSameInstant(ZoneOffset.UTC), + envId.toString()), + "anonymous")) + .thenReturn(pushResult); + // when + var path = "/push-api-activity-log"; + testClientHelper.postAndVerify( + (uriBuilder -> uriBuilder.path(path).build()), + request, + bodyStr -> { + // then + var result = + content(bodyStr, new TypeReference>() {}); + assertThat(result.getData()).isEqualTo(pushResult); + }); + } + + @Test + void givenApiActivityLogs_whenSearchPushHistory_thenReturnsOk() { + // given + var pageRequest = + getSearchPageRequest( + PagingHelper.DEFAULT_PAGE, PagingHelper.DEFAULT_SIZE, Sort.Direction.DESC, "createdAt"); + var historyList = List.of(new PushApiActivityLogHistory()); + when(service.searchHistory(PushLogSearchRequest.builder().build(), pageRequest)) + .thenReturn(PagingHelper.toPage(historyList, 0, 1)); + // when + var path = "/push-api-activity-log/history"; + testClientHelper.getAndVerify( + (uriBuilder -> uriBuilder.path(path).build()), + bodyStr -> { + // then + var result = + content( + bodyStr, new TypeReference>>() {}); + assertThat(result.getData().getData()).isEqualTo(historyList); + }); + } + + @Test + void givenPushApiLogEnabled_whenEnabled_thenReturnsTrue() { + // given + when(service.isPushApiActivityLogEnabled()).thenReturn(new PushApiActivityLogEnabled(true)); + // when + var path = "/push-api-activity-log/enabled"; + testClientHelper.getAndVerify( + (uriBuilder -> uriBuilder.path(path).build()), + bodyStr -> { + // then + var result = + content(bodyStr, new TypeReference>() {}); + assertThat(result.getData().isEnabled()).isTrue(); + }); + } + + @SneakyThrows + private T content(String response, TypeReference typeReference) { + return objectMapper.readValue(response, typeReference); + } +} diff --git a/kraken-java-sdk/kraken-java-sdk-controller/src/test/java/com/consoleconnect/kraken/operator/controller/service/push/ApiActivityPushServiceTest.java b/kraken-java-sdk/kraken-java-sdk-controller/src/test/java/com/consoleconnect/kraken/operator/controller/service/push/ApiActivityPushServiceTest.java new file mode 100644 index 00000000..4f0df7f1 --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-controller/src/test/java/com/consoleconnect/kraken/operator/controller/service/push/ApiActivityPushServiceTest.java @@ -0,0 +1,203 @@ +package com.consoleconnect.kraken.operator.controller.service.push; + +import static com.consoleconnect.kraken.operator.controller.service.push.ApiActivityPushService.PUSH_API_ACTIVITY_LOGS_IS_DISABLED; +import static com.consoleconnect.kraken.operator.controller.service.push.ApiActivityPushService.THE_SAME_PARAMETERS_ALREADY_EXISTS_ERROR; +import static com.consoleconnect.kraken.operator.core.service.UnifiedAssetService.getSearchPageRequest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doReturn; + +import com.consoleconnect.kraken.operator.config.TestApplication; +import com.consoleconnect.kraken.operator.controller.dto.push.CreatePushApiActivityRequest; +import com.consoleconnect.kraken.operator.controller.dto.push.PushApiActivityLogHistory; +import com.consoleconnect.kraken.operator.controller.model.Environment; +import com.consoleconnect.kraken.operator.controller.service.EnvironmentService; +import com.consoleconnect.kraken.operator.core.enums.EventStatusType; +import com.consoleconnect.kraken.operator.core.enums.MgmtEventType; +import com.consoleconnect.kraken.operator.core.exception.KrakenException; +import com.consoleconnect.kraken.operator.core.model.AppProperty; +import com.consoleconnect.kraken.operator.core.repo.MgmtEventRepository; +import com.consoleconnect.kraken.operator.core.request.PushLogSearchRequest; +import com.consoleconnect.kraken.operator.core.toolkit.JsonToolkit; +import com.consoleconnect.kraken.operator.core.toolkit.PagingHelper; +import com.consoleconnect.kraken.operator.test.AbstractIntegrationTest; +import com.consoleconnect.kraken.operator.test.MockIntegrationTest; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +@ActiveProfiles("test-auth-server-enabled") +@MockIntegrationTest +@ContextConfiguration(classes = TestApplication.class) +class ApiActivityPushServiceTest extends AbstractIntegrationTest { + + @Autowired private ApiActivityPushService sut; + @Autowired private MgmtEventRepository mgmtEventRepository; + @Autowired private EnvironmentService environmentService; + @SpyBean private AppProperty appProperty; + + @Test + void givenApiLogSearchParam_whenCreatePushApiActivityLogInfo_thenSaveEvent() { + // given + var env = environmentService.findAll().get(0); + var userId = "userId1"; + var endTime = ZonedDateTime.parse("2024-10-10T00:00:00+01:00"); + var startTime = endTime.minusDays(3); + var request = new CreatePushApiActivityRequest(startTime, endTime, env.getId()); + // when + var created = sut.createPushApiActivityLogInfo(request, userId); + // then + var byId = + mgmtEventRepository + .findById(created.getId()) + .orElseThrow( + () -> new RuntimeException("There should be entity with id: " + created.getId())); + assertThat(byId.getId()).isEqualTo(created.getId()); + assertThat(byId.getEventType()).isEqualTo(MgmtEventType.PUSH_API_ACTIVITY_LOG.name()); + assertThat(byId.getStatus()).isEqualTo(EventStatusType.ACK.name()); + var payload = JsonToolkit.fromJson(byId.getPayload(), PushLogActivityLogInfo.class); + assertThat(payload.getUser()).isEqualTo(userId); + assertThat(payload.getStartTime()).isEqualTo(toUtcString(startTime)); + assertThat(payload.getEndTime()).isEqualTo(toUtcString(endTime)); + assertThat(payload.getEnvId()).isEqualTo(env.getId()); + assertThat(payload.getEnvName()).isEqualTo(env.getName()); + } + + @Test + void + givenApiLogSearchParamForTheSameEnvAndSameTimeRangeInACKStatus_whenCreatePushApiActivityLogInfo_thenError() { + // given + var env = environmentService.findAll().get(0); + var userId = "userId1"; + var endTime = ZonedDateTime.parse("2024-10-10T00:00:00+01:00").minusDays(1); + var startTime = endTime.minusDays(3); + var request = new CreatePushApiActivityRequest(startTime, endTime, env.getId()); + sut.createPushApiActivityLogInfo(request, userId); + // when + var krakenException = + assertThrows( + KrakenException.class, () -> sut.createPushApiActivityLogInfo(request, userId)); + // then + assertThat(krakenException.getCode()).isEqualTo(400); + } + + @Test + void + givenApiLogSearchParamForTheSameEnvAndSameTimeRangeInProgressStatus_whenCreatePushApiActivityLogInfo_thenError() { + // given + var env = environmentService.findAll().get(0); + var userId = "userId1"; + var endTime = ZonedDateTime.parse("2024-10-10T00:00:00+01:00").minusDays(2); + var startTime = endTime.minusDays(3); + var request = new CreatePushApiActivityRequest(startTime, endTime, env.getId()); + var pushApiActivityLogInfo = sut.createPushApiActivityLogInfo(request, userId); + var mgmtEventEntity = + mgmtEventRepository + .findById(pushApiActivityLogInfo.getId()) + .orElseThrow(() -> new RuntimeException("Push log not found.")); + mgmtEventEntity.setStatus(EventStatusType.IN_PROGRESS.name()); + mgmtEventRepository.save(mgmtEventEntity); + // whe + var krakenException = + assertThrows( + KrakenException.class, () -> sut.createPushApiActivityLogInfo(request, userId)); + // then + assertThat(krakenException.getCode()).isEqualTo(400); + assertThat(krakenException.getMessage()).isEqualTo(THE_SAME_PARAMETERS_ALREADY_EXISTS_ERROR); + } + + @Test + void + givenPushedApiActivityLogs_whenCreatePushApiActivityLogInfo_thenReturnPushLogHistoryInDescOrder() { + // given + givenPushApiActivityLogs(); + var pageRequest = + getSearchPageRequest( + PagingHelper.DEFAULT_PAGE, PagingHelper.DEFAULT_SIZE, Sort.Direction.DESC, "createdAt"); + // when + var result = sut.searchHistory(PushLogSearchRequest.builder().build(), pageRequest); + // then + var logs = result.getData(); + assertThat(logs).hasSizeGreaterThan(2); + verifyIfLogsOrderedByCreatedAtDesc(logs); + } + + @Test + void givenPushApiActivityLogEnabled_whenIsPushApiActivityLogEnabled_thenReturnTrue() { + // given + // when + var result = sut.isPushApiActivityLogEnabled(); + // then + assertThat(result.isEnabled()).isTrue(); + } + + @Test + void givenPushApiActivityLogDisabled_whenIsPushApiActivityLogEnabled_thenReturnFalse() { + // given + givenDisabledPushActivityLogExternal(); + // when + var result = sut.isPushApiActivityLogEnabled(); + // then + assertThat(result.isEnabled()).isFalse(); + } + + @Test + void givenPushApiActivityLogDisabled_createPushApiActivityLogInfo_thenReturnsError() { + // given + givenDisabledPushActivityLogExternal(); + var env = environmentService.findAll().get(0); + var userId = "userId1"; + var endTime = ZonedDateTime.parse("2024-10-10T00:00:00+01:00").minusDays(1); + var startTime = endTime.minusDays(3); + var request = new CreatePushApiActivityRequest(startTime, endTime, env.getId()); + // when + var krakenException = + assertThrows( + KrakenException.class, () -> sut.createPushApiActivityLogInfo(request, userId)); + // then + assertThat(krakenException.getCode()).isEqualTo(400); + assertThat(krakenException.getMessage()).isEqualTo(PUSH_API_ACTIVITY_LOGS_IS_DISABLED); + } + + private void givenDisabledPushActivityLogExternal() { + AppProperty.PushActivityLogExternal pushActivityLogExternal = + new AppProperty.PushActivityLogExternal(); + pushActivityLogExternal.setEnabled(false); + AppProperty.Features appFeatures = new AppProperty.Features(); + appFeatures.setPushActivityLogExternal(pushActivityLogExternal); + doReturn(appFeatures).when(appProperty).getFeatures(); + } + + private void verifyIfLogsOrderedByCreatedAtDesc(List logs) { + for (int i = 0; i < logs.size() - 1; i++) { + assertThat(logs.get(i).getCreatedAt().isAfter(logs.get(i + 1).getCreatedAt())).isTrue(); + } + } + + private void givenPushApiActivityLogs() { + var env = environmentService.findAll().get(0); + for (int i = 1; i < 4; i++) { + var request = pushApiActivityRequest(env, i); + sut.createPushApiActivityLogInfo(request, "userId1"); + } + } + + private CreatePushApiActivityRequest pushApiActivityRequest(Environment env, int i) { + var endTime = ZonedDateTime.now().minusDays(i + 5); + var startTime = endTime.minusDays(3); + return new CreatePushApiActivityRequest(startTime, endTime, env.getId()); + } + + private String toUtcString(ZonedDateTime zonedDateTime) { + return zonedDateTime + .withZoneSameInstant(ZoneOffset.UTC) + .format(DateTimeFormatter.ISO_ZONED_DATE_TIME); + } +} diff --git a/kraken-java-sdk/kraken-java-sdk-controller/src/test/resources/application.yml b/kraken-java-sdk/kraken-java-sdk-controller/src/test/resources/application.yml index 918eeb14..a6e3205b 100644 --- a/kraken-java-sdk/kraken-java-sdk-controller/src/test/resources/application.yml +++ b/kraken-java-sdk/kraken-java-sdk-controller/src/test/resources/application.yml @@ -115,7 +115,9 @@ app: endpoints: exposure: include: asset, component, product, ingestion, component-operation, component-mapper - + features: + push-activity-log-external: + enabled: true security: resource-server: enabled: false diff --git a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/enums/EventStatusType.java b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/enums/EventStatusType.java index 8d5f1c7d..9326dc09 100644 --- a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/enums/EventStatusType.java +++ b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/enums/EventStatusType.java @@ -2,6 +2,7 @@ public enum EventStatusType { ACK, + IN_PROGRESS, DONE, FAILED, WAIT_TO_SEND; diff --git a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/enums/MgmtEventType.java b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/enums/MgmtEventType.java index 846c4560..0759b615 100644 --- a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/enums/MgmtEventType.java +++ b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/enums/MgmtEventType.java @@ -5,5 +5,6 @@ public enum MgmtEventType { CLIENT_HEART_BEAT, CLIENT_SYSTEM_INFO, TEMPLATE_UPGRADE_RESULT, - CLIENT_APP_VERSION_UPGRADE_RESULT; + CLIENT_APP_VERSION_UPGRADE_RESULT, + PUSH_API_ACTIVITY_LOG; } diff --git a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/model/AppProperty.java b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/model/AppProperty.java index 001fc13b..6b1fa677 100644 --- a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/model/AppProperty.java +++ b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/model/AppProperty.java @@ -19,4 +19,15 @@ public class AppProperty { private Map apiSpecOrderBy = new HashMap<>(); private Map apiOrderBy = new HashMap<>(); private Map apiTargetMapperOrderBy = new HashMap<>(); + private Features features; + + @Data + public static class Features { + private PushActivityLogExternal pushActivityLogExternal; + } + + @Data + public static class PushActivityLogExternal { + private boolean enabled; + } } diff --git a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/repo/ApiActivityLogRepository.java b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/repo/ApiActivityLogRepository.java index f5a79125..9be3d31f 100644 --- a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/repo/ApiActivityLogRepository.java +++ b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/repo/ApiActivityLogRepository.java @@ -44,4 +44,6 @@ List findTopEndpoints( @Param("callSeq") String callSeq, @Param("buyer") String buyer, @Param("limit") int limit); + + List findAllByRequestIdIn(List requestIds); } diff --git a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/repo/MgmtEventRepository.java b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/repo/MgmtEventRepository.java index 35ad03d5..f21c879a 100644 --- a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/repo/MgmtEventRepository.java +++ b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/repo/MgmtEventRepository.java @@ -2,6 +2,7 @@ import com.consoleconnect.kraken.operator.core.entity.MgmtEventEntity; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -26,4 +27,24 @@ Page search( Page findByEventTypeInAndStatus( List mgmtEventTypeList, String eventStatusType, Pageable pageable); + + Optional findFirstByEventTypeAndStatus(String eventType, String status); + + @Query( + value = + "SELECT EXISTS (" + + "select * from kraken_mgmt_event e WHERE" + + " e.status in :status " + + " AND e.event_type = :eventType " + + " AND e.payload->>'envId' = :envId " + + " AND e.payload->>'startTime' = :startTime " + + " AND e.payload->>'endTime' = :endTime " + + ")", + nativeQuery = true) + boolean existsBy( + @Param("status") List status, + @Param("envId") String envId, + @Param("startTime") String startTime, + @Param("endTime") String endTime, + @Param("eventType") String eventType); } diff --git a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/request/PushLogSearchRequest.java b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/request/PushLogSearchRequest.java new file mode 100644 index 00000000..914d58fc --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/request/PushLogSearchRequest.java @@ -0,0 +1,12 @@ +package com.consoleconnect.kraken.operator.core.request; + +import java.time.ZonedDateTime; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class PushLogSearchRequest { + ZonedDateTime queryStart; + ZonedDateTime queryEnd; +} diff --git a/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/model/SyncProperty.java b/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/model/SyncProperty.java index b447b861..3c85566c 100644 --- a/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/model/SyncProperty.java +++ b/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/model/SyncProperty.java @@ -29,6 +29,8 @@ public static class ControlPlane { private String pushEventEndpoint = "/client/events"; private String triggerInstallationEndpoint = "/v2/callback/triggers/installation"; + + private PushActivityLogExternal pushActivityLogExternal; } @Data @@ -37,4 +39,10 @@ public static class MgmtPlane { private String downloadMappingTemplateEndpoint = "/callback/agent/mapping-template-download"; private String mgmtPushEventEndpoint = "/callback/agent/events"; } + + @Data + public static class PushActivityLogExternal { + private boolean enabled; + private int batchSize = 200; + } } diff --git a/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/service/push/PushAPIActivityLogScheduler.java b/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/service/push/PushAPIActivityLogScheduler.java new file mode 100644 index 00000000..a9f05afa --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/service/push/PushAPIActivityLogScheduler.java @@ -0,0 +1,191 @@ +package com.consoleconnect.kraken.operator.sync.service.push; + +import static com.consoleconnect.kraken.operator.core.toolkit.JsonToolkit.fromJson; +import static com.consoleconnect.kraken.operator.core.toolkit.PagingHelper.toPageNoSubList; +import static java.util.Collections.emptyList; + +import com.consoleconnect.kraken.operator.core.dto.ApiActivityLog; +import com.consoleconnect.kraken.operator.core.dto.ComposedHttpRequest; +import com.consoleconnect.kraken.operator.core.entity.AbstractHttpEntity; +import com.consoleconnect.kraken.operator.core.entity.ApiActivityLogEntity; +import com.consoleconnect.kraken.operator.core.entity.MgmtEventEntity; +import com.consoleconnect.kraken.operator.core.enums.EventStatusType; +import com.consoleconnect.kraken.operator.core.enums.MgmtEventType; +import com.consoleconnect.kraken.operator.core.exception.KrakenException; +import com.consoleconnect.kraken.operator.core.mapper.ApiActivityLogMapper; +import com.consoleconnect.kraken.operator.core.model.HttpResponse; +import com.consoleconnect.kraken.operator.core.repo.ApiActivityLogRepository; +import com.consoleconnect.kraken.operator.core.repo.MgmtEventRepository; +import com.consoleconnect.kraken.operator.sync.model.SyncProperty; +import com.consoleconnect.kraken.operator.sync.service.KrakenServerConnector; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.http.HttpMethod; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriBuilder; + +@Service +@Slf4j +@ConditionalOnProperty( + value = "app.control-plane.push-activity-log-external.enabled", + havingValue = "true") +public class PushAPIActivityLogScheduler extends KrakenServerConnector { + + public static final String CALL_SEQ = "callSeq"; + public static final String CALL_SEQ_ZERO = "0"; + public static final String ENV = "env"; + public static final String CREATED_AT = "createdAt"; + + private final MgmtEventRepository mgmtEventRepository; + private final ApiActivityLogRepository apiActivityLogRepository; + + public PushAPIActivityLogScheduler( + SyncProperty appProperty, + WebClient webClient, + MgmtEventRepository mgmtEventRepository, + ApiActivityLogRepository apiActivityLogRepository) { + super(appProperty, webClient); + this.mgmtEventRepository = mgmtEventRepository; + this.apiActivityLogRepository = apiActivityLogRepository; + } + + @Scheduled(cron = "${app.cron-job.push-log-external-system:-}") + List pushApiActivityLogToExternalSystem() { + Optional entity = + mgmtEventRepository.findFirstByEventTypeAndStatus( + MgmtEventType.PUSH_API_ACTIVITY_LOG.name(), EventStatusType.ACK.name()); + if (entity.isPresent()) { + log.info("Start pushing log to external system for event id: {}", entity.get().getId()); + var start = ZonedDateTime.now(); + var sent = pushLogs(entity.get()); + log.info( + "End pushing log to external system for event id: {} in {} seconds.", + entity.get().getId(), + Duration.between(start, ZonedDateTime.now()).getSeconds()); + return sent; + } else { + log.info("No push api activity log event found."); + return emptyList(); + } + } + + private List pushLogs(MgmtEventEntity mgmtEvent) { + mgmtEvent.setStatus(EventStatusType.IN_PROGRESS.name()); + mgmtEventRepository.save(mgmtEvent); + var logInfo = fromJson(mgmtEvent.getPayload(), PushLogActivityLogInfo.class); + try { + var sent = pushLogsInBatches(logInfo, mgmtEvent.getId()); + mgmtEvent.setStatus(EventStatusType.DONE.name()); + mgmtEventRepository.save(mgmtEvent); + return sent; + } catch (Exception ex) { + mgmtEvent.setStatus(EventStatusType.FAILED.name()); + mgmtEventRepository.save(mgmtEvent); + return emptyList(); + } + } + + private List pushLogsInBatches( + PushLogActivityLogInfo logInfo, UUID eventId) { + int page = 0; + var sent = new ArrayList(); + while (true) { + var pageable = + PageRequest.of( + page, getAppProperty().getControlPlane().getPushActivityLogExternal().getBatchSize()); + var entities = getApiActivityLogRequestIds(logInfo, pageable); + var composedLogs = getComposedHttpRequests(entities.get()); + if (composedLogs.isEmpty()) { + log.info("No API activity logs found to push to external system."); + break; + } + var payload = + new PushExternalSystemPayload( + eventId, + logInfo.getStartTime(), + logInfo.getEndTime(), + logInfo.getEnvName(), + toPageNoSubList( + composedLogs, + entities.getNumber(), + entities.getSize(), + entities.getTotalElements())); + var res = sendLogsToExternalSystem(payload); + if (res.getCode() != 200) { + throw new KrakenException( + 400, "Pushing logs to external system filed with status: " + res.getCode()); + } + page++; + sent.add(payload); + } + return sent; + } + + private HttpResponse sendLogsToExternalSystem(PushExternalSystemPayload payload) { + return blockCurl( + HttpMethod.POST, UriBuilder::build, payload, new ParameterizedTypeReference<>() {}); + } + + private List getComposedHttpRequests(Stream logs) { + var requestIds = logs.map(AbstractHttpEntity::getRequestId).toList(); + return this.apiActivityLogRepository.findAllByRequestIdIn(requestIds).stream() + .map(ApiActivityLogMapper.INSTANCE::map) + .collect(Collectors.groupingBy(ApiActivityLog::getRequestId)) + .values() + .stream() + .map( + apiActivityLogs -> { + apiActivityLogs.sort(Comparator.comparing(ApiActivityLog::getCallSeq)); + ComposedHttpRequest composedHttpRequest = new ComposedHttpRequest(); + composedHttpRequest.setMain(apiActivityLogs.get(0)); + if (apiActivityLogs.size() > 1) { + composedHttpRequest.setBranches(apiActivityLogs.subList(1, apiActivityLogs.size())); + } + return composedHttpRequest; + }) + .toList(); + } + + private Page getApiActivityLogRequestIds( + PushLogActivityLogInfo logInfo, Pageable pageable) { + Specification spec = + (root, query, criteriaBuilder) -> { + var predicateList = predicates(logInfo, root, criteriaBuilder); + return query.where(predicateList.toArray(new Predicate[0])).getRestriction(); + }; + return apiActivityLogRepository.findAll(spec, pageable); + } + + private List predicates( + PushLogActivityLogInfo logInfo, + Root root, + CriteriaBuilder criteriaBuilder) { + var predicateList = new ArrayList(); + predicateList.add(criteriaBuilder.equal(root.get(ENV), logInfo.getEnvId())); + predicateList.add(criteriaBuilder.equal(root.get(CALL_SEQ), CALL_SEQ_ZERO)); + predicateList.add( + criteriaBuilder.greaterThanOrEqualTo(root.get(CREATED_AT), logInfo.getStartTime())); + predicateList.add( + criteriaBuilder.lessThanOrEqualTo(root.get(CREATED_AT), logInfo.getEndTime())); + return predicateList; + } +} diff --git a/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/service/push/PushExternalSystemPayload.java b/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/service/push/PushExternalSystemPayload.java new file mode 100644 index 00000000..cd18ae8f --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/service/push/PushExternalSystemPayload.java @@ -0,0 +1,18 @@ +package com.consoleconnect.kraken.operator.sync.service.push; + +import com.consoleconnect.kraken.operator.core.dto.ComposedHttpRequest; +import com.consoleconnect.kraken.operator.core.toolkit.Paging; +import java.time.ZonedDateTime; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class PushExternalSystemPayload { + private UUID id; + private ZonedDateTime startTime; + private ZonedDateTime endTime; + private String envName; + private Paging data; +} diff --git a/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/service/push/PushLogActivityLogInfo.java b/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/service/push/PushLogActivityLogInfo.java new file mode 100644 index 00000000..4385bac8 --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-sync/src/main/java/com/consoleconnect/kraken/operator/sync/service/push/PushLogActivityLogInfo.java @@ -0,0 +1,13 @@ +package com.consoleconnect.kraken.operator.sync.service.push; + +import java.time.ZonedDateTime; +import lombok.Data; + +@Data +public class PushLogActivityLogInfo { + private String user; + private ZonedDateTime startTime; + private String envId; + private String envName; + private ZonedDateTime endTime; +} diff --git a/kraken-java-sdk/kraken-java-sdk-sync/src/test/java/com/consoleconnect/kraken/operator/sync/service/push/PushAPIActivityLogSchedulerTest.java b/kraken-java-sdk/kraken-java-sdk-sync/src/test/java/com/consoleconnect/kraken/operator/sync/service/push/PushAPIActivityLogSchedulerTest.java new file mode 100644 index 00000000..6b090ad3 --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-sync/src/test/java/com/consoleconnect/kraken/operator/sync/service/push/PushAPIActivityLogSchedulerTest.java @@ -0,0 +1,307 @@ +package com.consoleconnect.kraken.operator.sync.service.push; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.consoleconnect.kraken.operator.core.entity.ApiActivityLogEntity; +import com.consoleconnect.kraken.operator.core.entity.MgmtEventEntity; +import com.consoleconnect.kraken.operator.core.enums.EventStatusType; +import com.consoleconnect.kraken.operator.core.enums.MgmtEventType; +import com.consoleconnect.kraken.operator.core.model.HttpResponse; +import com.consoleconnect.kraken.operator.core.repo.ApiActivityLogRepository; +import com.consoleconnect.kraken.operator.core.repo.MgmtEventRepository; +import com.consoleconnect.kraken.operator.core.toolkit.JsonToolkit; +import com.consoleconnect.kraken.operator.sync.CustomConfig; +import com.consoleconnect.kraken.operator.test.AbstractIntegrationTest; +import com.consoleconnect.kraken.operator.test.MockIntegrationTest; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import lombok.SneakyThrows; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +@MockIntegrationTest +@ContextConfiguration(classes = {CustomConfig.class}) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class PushAPIActivityLogSchedulerTest extends AbstractIntegrationTest { + + public static final String ENV_ID = "envId1"; + static MockWebServer mockWebServer = new MockWebServer(); + static final String NOW_WITH_TIMEZONE = "2023-10-24T05:00:00+02:00"; + static final String BUYER_ID_1 = "buyerId1"; + + @Autowired private PushAPIActivityLogScheduler sut; + @Autowired private ApiActivityLogRepository apiActivityLogRepository; + @Autowired private MgmtEventRepository mgmtEventRepository; + + @BeforeAll + @SneakyThrows + static void setUp() { + mockWebServer.start(4567); + } + + @AfterAll + @SneakyThrows + static void tearDown() { + mockWebServer.shutdown(); + } + + @Test + void givenApiLogs_whenPushApiActivityLogToExternalSystem_thenAllLogsSentAndEventInStatusDone() { + // given + givenExternalServerResponses(); + + var endTime = ZonedDateTime.parse(NOW_WITH_TIMEZONE); + var startTime = ZonedDateTime.parse(NOW_WITH_TIMEZONE).minusDays(1); + var logs = createLogs(toUTC(endTime), ENV_ID); + apiActivityLogRepository.saveAll(logs); + var logEvent = createPushApiActivityLogEvent(ENV_ID, startTime, endTime, "userId1"); + // when + var sent = sut.pushApiActivityLogToExternalSystem(); + // then + var done = + mgmtEventRepository + .findById(logEvent.getId()) + .orElseThrow( + () -> new RuntimeException("There should be entity with id: " + logEvent.getId())); + assertThat(done.getStatus()).isEqualTo(EventStatusType.DONE.name()); + assertThat(sent).hasSize(2); + verifyPage0(sent.get(0), logEvent); + verifyPage1(sent.get(1), logEvent); + } + + private void verifyPage0(PushExternalSystemPayload page0, MgmtEventEntity logEvent) { + assertThat(page0.getData().getTotal()).isEqualTo(40); + assertThat(page0.getData().getPage()).isZero(); + assertThat(page0.getData().getSize()).isEqualTo(30); + assertThat(page0.getData().getData()).hasSize(30); + verifyData(page0, logEvent); + } + + private static void verifyData(PushExternalSystemPayload page0, MgmtEventEntity logEvent) { + assertThat(page0.getId()).isEqualTo(logEvent.getId()); + var payload = JsonToolkit.fromJson(logEvent.getPayload(), PushLogActivityLogInfo.class); + assertThat(page0.getEnvName()).isEqualTo(payload.getEnvName()); + assertThat(page0.getStartTime()).isEqualTo(payload.getStartTime()); + assertThat(page0.getEndTime()).isEqualTo(payload.getEndTime()); + } + + private void verifyPage1(PushExternalSystemPayload page1, MgmtEventEntity logEvent) { + assertThat(page1.getData().getTotal()).isEqualTo(40); + assertThat(page1.getData().getPage()).isEqualTo(1); + assertThat(page1.getData().getSize()).isEqualTo(30); + assertThat(page1.getData().getData()).hasSize(10); + verifyData(page1, logEvent); + } + + @Test + void givenApiLogs_whenPushApiActivityLogToExternalSystemAndReturnError_thenEventInStatusFailed() { + // given + givenFailedExternalSystemResponse(); + var endTime = ZonedDateTime.parse(NOW_WITH_TIMEZONE); + var startTime = ZonedDateTime.parse(NOW_WITH_TIMEZONE).minusDays(1); + var logs = createLogs(toUTC(endTime), ENV_ID); + apiActivityLogRepository.saveAll(logs); + var logEvent = createPushApiActivityLogEvent(ENV_ID, startTime, endTime, "userId1"); + // when + var sent = sut.pushApiActivityLogToExternalSystem(); + // then + var done = + mgmtEventRepository + .findById(logEvent.getId()) + .orElseThrow( + () -> new RuntimeException("There should be entity with id: " + logEvent.getId())); + assertThat(done.getStatus()).isEqualTo(EventStatusType.FAILED.name()); + assertThat(sent).isEmpty(); + } + + private static void givenFailedExternalSystemResponse() { + mockResponse(500); + } + + private static void givenExternalServerResponses() { + IntStream.range(0, 2) + .forEach( + it -> { + mockResponse(200); + }); + } + + private static void mockResponse(int code) { + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(code); + mockResponse.setBody(JsonToolkit.toJson(HttpResponse.ok("Sync"))); + mockResponse.addHeader("Content-Type", "application/json"); + mockWebServer.enqueue(mockResponse); + } + + private List createLogs(ZonedDateTime now, String envId) { + var payload = new ArrayList(); + payload.addAll( + PayloadBuilder.builder() + .envId(envId) + .method("GET") + .path0("/mefApi/sonata/product/123") + .path1("/hub/product/123") + .httpStatus(200) + .now(now.minusHours(1)) + .number(10) + .buyerId(BUYER_ID_1) + .build() + .createPayload()); + payload.addAll( + PayloadBuilder.builder() + .envId(envId) + .method("GET") + .path0("/mefApi/sonata/product/234") + .path1("/hub/product/234") + .httpStatus(204) + .now(now.minusHours(2)) + .number(10) + .buyerId(BUYER_ID_1) + .build() + .createPayload()); + payload.addAll( + PayloadBuilder.builder() + .envId(envId) + .method("GET") + .path0("/mefApi/sonata/order/678") + .path1("/hub/order/678") + .httpStatus(401) + .now(now.minusHours(3)) + .number(10) + .buyerId(BUYER_ID_1) + .build() + .createPayload()); + payload.addAll( + PayloadBuilder.builder() + .envId(envId) + .method("GET") + .path0("/mefApi/sonata/product/789") + .path1("/hub/product/789") + .httpStatus(500) + .now(now.minusHours(4)) + .number(10) + .buyerId("buyer2") + .build() + .createPayload()); + return payload; + } + + @lombok.Builder + public static class PayloadBuilder { + private String method; + private String path0; + private String path1; + private int httpStatus; + private ZonedDateTime now; + private int number; + private String buyerId; + private String envId; + + List createPayload() { + return IntStream.range(0, number) + .mapToObj( + operand -> { + ZonedDateTime date = now.minusHours(operand); + return createEntity( + method, path0, path1, httpStatus, date, date.plusSeconds(2), buyerId, envId); + }) + .flatMap(stream -> stream) + .toList(); + } + + private Stream createEntity( + String method, + String path0, + String path1, + Integer httpStatus, + ZonedDateTime createdAt0, + ZonedDateTime createdAt1, + String buyerId, + String envId) { + var requestId = UUID.randomUUID(); + var apiActivityLog0 = + getApiActivityLog0(method, path0, httpStatus, createdAt0, requestId, buyerId, envId); + var apiActivityLog1 = + getApiActivityLog1(method, path1, httpStatus, createdAt1, requestId, buyerId, envId); + return Stream.of(apiActivityLog0, apiActivityLog1); + } + + private ApiActivityLogEntity getApiActivityLog0( + String method, + String path0, + Integer httpStatus, + ZonedDateTime createdAt0, + UUID requestId, + String buyerId, + String envId) { + var apiActivityLog0 = new ApiActivityLogEntity(); + apiActivityLog0.setEnv(envId); + apiActivityLog0.setRequestId(requestId.toString()); + apiActivityLog0.setCallSeq(0); + apiActivityLog0.setUri("http://localhost:8888/mef.sonata"); + apiActivityLog0.setMethod(method); + apiActivityLog0.setPath(path0); + apiActivityLog0.setHttpStatusCode(httpStatus); + apiActivityLog0.setCreatedAt(createdAt0); + apiActivityLog0.setBuyer(buyerId); + return apiActivityLog0; + } + + private ApiActivityLogEntity getApiActivityLog1( + String method, + String path1, + Integer httpStatus, + ZonedDateTime createdAt1, + UUID requestId, + String buyerId, + String envId) { + var apiActivityLog1 = new ApiActivityLogEntity(); + apiActivityLog1.setEnv(envId); + apiActivityLog1.setRequestId(requestId.toString()); + apiActivityLog1.setCallSeq(1); + apiActivityLog1.setUri("http://localhost:8888/mef.sonata"); + apiActivityLog1.setMethod(method); + apiActivityLog1.setPath(path1); + apiActivityLog1.setHttpStatusCode(httpStatus); + apiActivityLog1.setCreatedAt(createdAt1); + apiActivityLog1.setBuyer(buyerId); + return apiActivityLog1; + } + } + + private ZonedDateTime toUTC(ZonedDateTime now) { + return now.withZoneSameInstant(ZoneId.of("UTC")); + } + + public MgmtEventEntity createPushApiActivityLogEvent( + String envId, ZonedDateTime startTime, ZonedDateTime endTime, String userId) { + var entity = new MgmtEventEntity(); + entity.setStatus(EventStatusType.ACK.name()); + entity.setEventType(MgmtEventType.PUSH_API_ACTIVITY_LOG.name()); + entity.setPayload(createData(envId, startTime, endTime, userId)); + return mgmtEventRepository.save(entity); + } + + private PushLogActivityLogInfo createData( + String envId, ZonedDateTime startTime, ZonedDateTime endTime, String userId) { + var data = new PushLogActivityLogInfo(); + data.setUser(userId); + data.setEnvId(envId); + data.setStartTime(startTime); + data.setEndTime(endTime); + return data; + } +} diff --git a/kraken-java-sdk/kraken-java-sdk-sync/src/test/resources/application.yaml b/kraken-java-sdk/kraken-java-sdk-sync/src/test/resources/application.yaml index 8dc12b8e..03724933 100644 --- a/kraken-java-sdk/kraken-java-sdk-sync/src/test/resources/application.yaml +++ b/kraken-java-sdk/kraken-java-sdk-sync/src/test/resources/application.yaml @@ -85,6 +85,9 @@ app: controlPlane: url: http://localhost:4567 token: 123456 + push-activity-log-external: + batch-size: 30 + enabled: true accept-asset-kinds: - kraken.component.api-target task: