From b08003a5c696d2e5d4f336f2ecbd80661e5726a4 Mon Sep 17 00:00:00 2001 From: Akshat Soni Date: Wed, 26 Apr 2023 14:07:46 +0530 Subject: [PATCH] Akshat | TP-19860 | Tesseract Integration, Query integration including fetching metrics and showing on Frontend (#37) * TP-19860 | integrate tesseract client * TP-19860 | add stats path * TP-19860 | add RestClient * TP-19860 | move dtos to model * TP-19860 | add ExperimentMetricResultListener * TP-19860 | add metric processing * TP-19860 | add registerExperimentMetricQueries * TP-19860 | add totalUsers for metric result * TP-19860 | add table data extraction * TP-19860 | add apis for showing metrics on frontent * TP-19860 | change timestamps * TP-19860 | sort audit trail by created_at * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r106189 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r106190 * TP-19860 | change metric result design * TP-19860 | update experimentDataDto fetch * TP-19860 | move getTesseractQueryRegisterRequest to Tesseract Client * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r106198 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r106192 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r106201 * TP-19860 | add tesseractId status updation from tesseract service * TP-19860 | add duplicate metric exception * TP-19860 | remove failed lazy loading error * TP-19860 | refactor code * TP-19860 | remove dms dtos * TP-19860 | change cron to run between 10 PM to 4 PM * TP-19860 | add start and end time in necessary query variables * TP-19860 | add shedlock * TP-19860 | edit cron and add shedlock * TP-19860 | make tesseract_id index name unique * TP-19860 | add shedlock table * TP-19860 | add chi square test and experiment impact * TP-19860 | add name of CONTROL and TREATMENT to Constants * TP-19860 | fix tests * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108021 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108022 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108024 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108148 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108149 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108150 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108151 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108165 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108168 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108170 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108171 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108172 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108176 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108177 * TP-19860 | throw when query variables are invalid * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108181 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108183 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108185 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108186 && https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108187 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108259 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108273 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108274 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108182 * TP-19860 | change application.properties * TP-19860 | add max days before current date we get data of total Users * TP-19860 | fix tests * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108346 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108356 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108353 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108352 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108351 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108348 * TP-19860 | remove tags from tesseract_id * TP-19860 | add progress percent * TP-19860 | handle edge case of adding variants * TP-19860 | handle chi square test if no data * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108874 * TP-19860 | add timed annotation * TP-19860 | https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108882 resolved * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108880 * TP-19860 | better name for payload: * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r108873 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r109301 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r109308 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r109312 * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/37#discussion_r109315 * TP-19860 | add timed in tesseract client * TP-19860 | remove optional as function response --- litmus-core/pom.xml | 39 +++ .../main/java/com/navi/medici/Constants.java | 13 + .../main/java/com/navi/medici/LitmusApp.java | 4 + .../navi/medici/config/LitmusCoreConfig.java | 48 ++++ .../com/navi/medici/config/RestClient.java | 14 ++ .../navi/medici/config/SchedulerConfig.java | 21 ++ .../controller/v2/ExperimentControllerV2.java | 19 ++ .../medici/handler/RetryableException.java | 7 + .../ExperimentMetricResultListener.java | 38 +++ .../navi/medici/mapper/ExperimentMapper.java | 31 +-- .../ExperimentMetricResultScheduler.java | 38 +++ .../service/experiment/ExperimentService.java | 3 + .../experiment/ExperimentServiceImpl.java | 226 ++++++++++++++++- .../ExperimentMetricResultService.java | 14 ++ .../ExperimentMetricResultServiceImpl.java | 194 ++++++++++++++ .../service/metric/MetricServiceImpl.java | 8 + .../TesseractQueryProcessingService.java | 14 ++ .../TesseractQueryProcessingServiceImpl.java | 27 ++ .../medici/tesseract/TesseractClient.java | 236 ++++++++++++++++++ .../com/navi/medici/util/DateTimeUtil.java | 5 + .../medici/validator/ExperimentValidator.java | 22 +- .../src/main/resources/application.properties | 41 ++- .../test/java/com/navi/medici/TestUtils.java | 13 + .../medici/mapper/ExperimentMapperTest.java | 10 +- .../navi/medici/mapper/MetricMapperTest.java | 2 + .../experiment/ExperimentServiceImplTest.java | 8 +- .../navi/medici/entity/ExperimentEntity.java | 7 +- .../entity/ExperimentMetricMappingEntity.java | 10 + .../entity/ExperimentMetricResultEntity.java | 18 +- .../com/navi/medici/entity/MetricEntity.java | 14 ++ .../entity/TesseractIdStatusEntity.java | 41 +++ .../query/experiment/ExperimentQueryImpl.java | 17 +- .../query/experiment/IExperimentQuery.java | 2 + .../ExperimentMetricMappingQueryImpl.java | 24 ++ .../IExperimentMetricMappingQuery.java | 12 + .../ExperimentMetricResultQueryImpl.java | 26 ++ .../IExperimentMetricResultQuery.java | 14 ++ .../medici/query/metric/IMetricQuery.java | 2 + .../medici/query/metric/MetricQueryImpl.java | 5 + .../ITesseractIdStatusQuery.java | 11 + .../TesseractIdStatusQueryImpl.java | 27 ++ .../ExperimentMetricMappingRepository.java | 15 ++ .../ExperimentMetricResultRepository.java | 13 + .../TesseractIdStatusRepository.java | 12 + ...sseract-id-to-experiment-metric-result.sql | 7 + ...s-job-run-in-experiment-metric-mapping.sql | 5 + .../202304042036-add-variable-mapping.sql | 8 + ...7-alter-experiment-metric-result-table.sql | 10 + ...91625-create-tesseract-id-status-table.sql | 16 ++ .../202304132304-create-shedlock-table.sql | 11 + litmus-model/pom.xml | 4 + .../navi/medici/dto/DoughnutSectionDTO.java | 18 ++ .../navi/medici/dto/ExperimentDataDTO.java | 22 ++ .../java/com/navi/medici/dto/GraphDTO.java | 22 ++ .../com/navi/medici/dto/MetricResult.java | 8 +- .../java/com/navi/medici/dto/TableDTO.java | 20 ++ .../response/DashboardExperimentResponse.java | 1 + .../medici/response/ExperimentResponse.java | 1 + .../medici/tesseract/request/SyncType.java | 5 + .../request/TesseractCSVRegisterRequest.java | 56 +++++ .../tesseract/request/TesseractIdStatus.java | 10 + .../TesseractQueryRegisterRequest.java | 64 +++++ .../request/TesseractQueryUpdateRequest.java | 21 ++ .../tesseract/response/QueryStatus.java | 10 + .../response/TesseractIdStatusResponse.java | 17 ++ .../TesseractQueryDeleteResponse.java | 16 ++ .../TesseractQueryRegisterResponse.java | 16 ++ .../response/TesseractQueryStatsResponse.java | 44 ++++ .../TesseractQueryStatusResponse.java | 5 + .../TesseractQueryUpdateResponse.java | 16 ++ litmus-util/pom.xml | 53 ++-- ...ateMetricMappingToExperimentException.java | 7 + ...oExperimentsFoundForVerticalException.java | 4 - .../exceptions/StatisticalException.java | 7 + .../medici/freemarker/FreeMarkerUtil.java | 59 +++++ .../com/navi/medici/metrics/MetricsUtils.java | 13 + .../com/navi/medici/stats/ChiSquaredTest.java | 36 +++ 77 files changed, 1898 insertions(+), 79 deletions(-) create mode 100644 litmus-core/src/main/java/com/navi/medici/config/RestClient.java create mode 100644 litmus-core/src/main/java/com/navi/medici/config/SchedulerConfig.java create mode 100644 litmus-core/src/main/java/com/navi/medici/handler/RetryableException.java create mode 100644 litmus-core/src/main/java/com/navi/medici/listener/ExperimentMetricResultListener.java create mode 100644 litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentMetricResultScheduler.java create mode 100644 litmus-core/src/main/java/com/navi/medici/service/experimentmetricresult/ExperimentMetricResultService.java create mode 100644 litmus-core/src/main/java/com/navi/medici/service/experimentmetricresult/ExperimentMetricResultServiceImpl.java create mode 100644 litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingService.java create mode 100644 litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingServiceImpl.java create mode 100644 litmus-core/src/main/java/com/navi/medici/tesseract/TesseractClient.java create mode 100644 litmus-db/src/main/java/com/navi/medici/entity/TesseractIdStatusEntity.java create mode 100644 litmus-db/src/main/java/com/navi/medici/query/tesseractidstatus/ITesseractIdStatusQuery.java create mode 100644 litmus-db/src/main/java/com/navi/medici/query/tesseractidstatus/TesseractIdStatusQueryImpl.java create mode 100644 litmus-db/src/main/java/com/navi/medici/repository/TesseractIdStatusRepository.java create mode 100644 litmus-liquibase/src/main/resources/db/changelog/202304031733-add-tesseract-id-to-experiment-metric-result.sql create mode 100644 litmus-liquibase/src/main/resources/db/changelog/202304041852-add-is-job-run-in-experiment-metric-mapping.sql create mode 100644 litmus-liquibase/src/main/resources/db/changelog/202304042036-add-variable-mapping.sql create mode 100644 litmus-liquibase/src/main/resources/db/changelog/202304061847-alter-experiment-metric-result-table.sql create mode 100644 litmus-liquibase/src/main/resources/db/changelog/202304091625-create-tesseract-id-status-table.sql create mode 100644 litmus-liquibase/src/main/resources/db/changelog/202304132304-create-shedlock-table.sql create mode 100644 litmus-model/src/main/java/com/navi/medici/dto/DoughnutSectionDTO.java create mode 100644 litmus-model/src/main/java/com/navi/medici/dto/ExperimentDataDTO.java create mode 100644 litmus-model/src/main/java/com/navi/medici/dto/GraphDTO.java create mode 100644 litmus-model/src/main/java/com/navi/medici/dto/TableDTO.java create mode 100644 litmus-model/src/main/java/com/navi/medici/tesseract/request/SyncType.java create mode 100644 litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractCSVRegisterRequest.java create mode 100644 litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractIdStatus.java create mode 100644 litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractQueryRegisterRequest.java create mode 100644 litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractQueryUpdateRequest.java create mode 100644 litmus-model/src/main/java/com/navi/medici/tesseract/response/QueryStatus.java create mode 100644 litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractIdStatusResponse.java create mode 100644 litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryDeleteResponse.java create mode 100644 litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryRegisterResponse.java create mode 100644 litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryStatsResponse.java create mode 100644 litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryStatusResponse.java create mode 100644 litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryUpdateResponse.java create mode 100644 litmus-util/src/main/java/com/navi/medici/exceptions/DuplicateMetricMappingToExperimentException.java create mode 100644 litmus-util/src/main/java/com/navi/medici/exceptions/StatisticalException.java create mode 100644 litmus-util/src/main/java/com/navi/medici/freemarker/FreeMarkerUtil.java create mode 100644 litmus-util/src/main/java/com/navi/medici/metrics/MetricsUtils.java create mode 100644 litmus-util/src/main/java/com/navi/medici/stats/ChiSquaredTest.java diff --git a/litmus-core/pom.xml b/litmus-core/pom.xml index af1aa91..de62724 100644 --- a/litmus-core/pom.xml +++ b/litmus-core/pom.xml @@ -40,6 +40,24 @@ 2.0.8-RELEASE + + com.navi.medici + litmus-util + 2.0.8-RELEASE + + + + io.github.resilience4j + resilience4j-micrometer + 1.7.1 + + + + io.github.resilience4j + resilience4j-retry + 1.7.0 + + com.navi.medici litmus-cache @@ -63,6 +81,11 @@ 4.0.1 + + org.springframework.kafka + spring-kafka + + software.amazon.awssdk s3 @@ -113,6 +136,22 @@ test + + net.javacrumbs.shedlock + shedlock-spring + 2.2.0 + + + net.javacrumbs.shedlock + shedlock-provider-jdbc-template + 2.1.0 + + + com.h2database + h2 + 1.4.200 + + diff --git a/litmus-core/src/main/java/com/navi/medici/Constants.java b/litmus-core/src/main/java/com/navi/medici/Constants.java index bbc42ae..0794b51 100644 --- a/litmus-core/src/main/java/com/navi/medici/Constants.java +++ b/litmus-core/src/main/java/com/navi/medici/Constants.java @@ -3,6 +3,8 @@ package com.navi.medici; public class Constants { public static final String ALL = "ALL"; public static final String HEADER_EMAIL_ID = "X-Email-Id"; + public static final String CONTROL = "CONTROL"; + public static final String TREATMENT = "TREATMENT"; //Update Messages public static final String EXPERIMENT_CREATED = "Experiment created"; @@ -13,4 +15,15 @@ public class Constants { public static final String PAUSE_EXPERIMENT = "Experiment has been paused"; public static final String METRIC_ATTACHED = "Metric %s attached to experiment"; public static final String EXPERIMENT_RESTARTED = "Experiment Restarted"; + + //Necessary Query Variables + public static final String EXPERIMENT_NAME = "experimentName"; + public static final String EXPERIMENT_CREATED_AT = "experimentCreatedAt"; + public static final String EXPERIMENT_ID = "experimentId"; + public static final String METRIC_ID = "metricId"; + public static final String SCHEDULE_TIME = "scheduleTime"; + public static final String START_TIME = "startTime"; + public static final String END_TIME = "endTime"; + public static final String DAY_INTERVAL_FOR_TOTAL_USERS = "dayIntervalForTotalUsers"; + } diff --git a/litmus-core/src/main/java/com/navi/medici/LitmusApp.java b/litmus-core/src/main/java/com/navi/medici/LitmusApp.java index a87497e..0e07e3a 100644 --- a/litmus-core/src/main/java/com/navi/medici/LitmusApp.java +++ b/litmus-core/src/main/java/com/navi/medici/LitmusApp.java @@ -1,9 +1,13 @@ package com.navi.medici; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling +@EnableSchedulerLock(defaultLockAtMostFor = "PT30S") public class LitmusApp { public static void main(String[] args) { SpringApplication.run(LitmusApp.class, args); diff --git a/litmus-core/src/main/java/com/navi/medici/config/LitmusCoreConfig.java b/litmus-core/src/main/java/com/navi/medici/config/LitmusCoreConfig.java index ea8cdf4..3e42f0e 100644 --- a/litmus-core/src/main/java/com/navi/medici/config/LitmusCoreConfig.java +++ b/litmus-core/src/main/java/com/navi/medici/config/LitmusCoreConfig.java @@ -21,4 +21,52 @@ public class LitmusCoreConfig { @Value("#{'${litmus.portal.allowed.methods}'.split(',')}") String[] litmusPortalAllowedMethods; + + @Value("${tesseract.register.job.max.retries}") + int tesseractRegisterJobMaxRetries; + + @Value("${tesseract.host}") + String tesseractHost; + + @Value("${tesseract.register.path}") + String tesseractRegisterPath; + + @Value("${tesseract.team.name}") + String tesserractTeamName; + + @Value("${tesseract.update.path}") + String tesseractUpdatePath; + + @Value("${tesseract.status.path}") + String tesseractStatusPath; + + @Value("${tesseract.stats.path}") + String tesseractStatsPath; + + @Value("${kafka.experiment.metric.result.topic}") + private String experimentMetricResultTopic; + + @Value("${tesseract.litmus.cluster.name}") + private String tesseractClusterName; + + @Value("${population.graph.data.days.interval}") + private int populationGraphDataDaysInterval; + + @Value("${litmus.core.base.url}") + private String litmusCoreBaseUrl; + + @Value("${query.schedule.and.end.time.diff.hours}") + int queryScheduleAndEndTimeDiff; + + @Value("${tesseract.interval.function.initial.interval}") + long intervalFunctionInitialInterval; + + @Value("${tesseract.interval.function.max.interval}") + long intervalFunctionMaxInterval; + + @Value("${tesseract.interval.function.multiplier}") + double intervalFunctionMultiplier; + + @Value("${query.day.interval.for.total.users}") + int queryDayIntervalForTotalUsers; } diff --git a/litmus-core/src/main/java/com/navi/medici/config/RestClient.java b/litmus-core/src/main/java/com/navi/medici/config/RestClient.java new file mode 100644 index 0000000..d646ff2 --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/config/RestClient.java @@ -0,0 +1,14 @@ +package com.navi.medici.config; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestClient { + @Bean + public RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder.build(); + } +} \ No newline at end of file diff --git a/litmus-core/src/main/java/com/navi/medici/config/SchedulerConfig.java b/litmus-core/src/main/java/com/navi/medici/config/SchedulerConfig.java new file mode 100644 index 0000000..68971d0 --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/config/SchedulerConfig.java @@ -0,0 +1,21 @@ +package com.navi.medici.config; + +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; + +@Configuration +public class SchedulerConfig { + @Bean + public LockProvider lockProvider(DataSource dataSource) { + return new JdbcTemplateLockProvider( + JdbcTemplateLockProvider.Configuration.builder() + .withJdbcTemplate(new JdbcTemplate(dataSource)) + .build() + ); + } +} diff --git a/litmus-core/src/main/java/com/navi/medici/controller/v2/ExperimentControllerV2.java b/litmus-core/src/main/java/com/navi/medici/controller/v2/ExperimentControllerV2.java index 53df3ff..36aec45 100644 --- a/litmus-core/src/main/java/com/navi/medici/controller/v2/ExperimentControllerV2.java +++ b/litmus-core/src/main/java/com/navi/medici/controller/v2/ExperimentControllerV2.java @@ -2,6 +2,7 @@ package com.navi.medici.controller.v2; import com.navi.medici.Constants; import com.navi.medici.dto.ExperimentAuditTrailDTO; +import com.navi.medici.dto.ExperimentDataDTO; import com.navi.medici.enums.ExperimentStatus; import com.navi.medici.request.v1.AttachMetricToExperimentRequest; import com.navi.medici.request.v1.CreateExperimentRequest; @@ -11,6 +12,8 @@ import com.navi.medici.response.DashboardExperimentResponse; import com.navi.medici.response.ExperimentResponse; import com.navi.medici.response.PaginatedSearchResponse; import com.navi.medici.service.experiment.ExperimentService; +import com.navi.medici.service.experimentmetricresult.ExperimentMetricResultService; +import com.navi.medici.tesseract.response.TesseractIdStatusResponse; import com.navi.medici.util.ControllerUtils; import io.micrometer.core.annotation.Timed; import lombok.RequiredArgsConstructor; @@ -37,6 +40,7 @@ import java.util.List; public class ExperimentControllerV2 { private final ExperimentService experimentService; + private final ExperimentMetricResultService experimentMetricResultService; @GetMapping @Timed(value = "litmus.get.experiments", percentiles = {0.95, 0.99}) @@ -122,7 +126,22 @@ public class ExperimentControllerV2 { } @GetMapping("/audit-trail/{experimentId}") + @Timed(value = "litmus.get.experiments.audit.trail", percentiles = {0.95, 0.99}) public ResponseEntity> getExperimentAuditTrail(@PathVariable("experimentId") String experimentId) { return ResponseEntity.ok(experimentService.getExperimentAuditTrail(experimentId)); } + + @GetMapping("/data/{experimentId}") + @Timed(value = "litmus.get.experiments.stats.data", percentiles = {0.95, 0.99}) + public ResponseEntity getExperimentStatsData(@PathVariable("experimentId") String experimentId) { + log.info("fetch request received for data of experimentId: {}", experimentId); + return ResponseEntity.ok(experimentService.getExperimentStatsData(experimentId)); + } + + @PostMapping("/tesseract-id-status") + @Timed(value = "tesseract.id.status.update", percentiles = {0.95, 0.99}) + public void updateStatusOfTesseractId(TesseractIdStatusResponse response) { + log.info("updating status of tesseract_id: {} to {}", response.getTesseractId(), response.getStatus().toString()); + experimentMetricResultService.updateStatusOfTesseractId(response); + } } diff --git a/litmus-core/src/main/java/com/navi/medici/handler/RetryableException.java b/litmus-core/src/main/java/com/navi/medici/handler/RetryableException.java new file mode 100644 index 0000000..df7ec9c --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/handler/RetryableException.java @@ -0,0 +1,7 @@ +package com.navi.medici.handler; + +public class RetryableException extends RuntimeException { + public RetryableException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/litmus-core/src/main/java/com/navi/medici/listener/ExperimentMetricResultListener.java b/litmus-core/src/main/java/com/navi/medici/listener/ExperimentMetricResultListener.java new file mode 100644 index 0000000..09d8213 --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/listener/ExperimentMetricResultListener.java @@ -0,0 +1,38 @@ +package com.navi.medici.listener; + +import com.navi.medici.metrics.MetricsUtils; +import com.navi.medici.service.experimentmetricresult.ExperimentMetricResultService; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.support.KafkaHeaders; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Component; + +@Component +@Log4j2 +@RequiredArgsConstructor +public class ExperimentMetricResultListener { + + private final ExperimentMetricResultService experimentMetricResultService; + private final MeterRegistry meterRegistry; + + @KafkaListener(topics = "${kafka.experiment.metric.result.topic}", groupId = "${kafka.litmus.consumer.group}") + public void listenMetricResultFromTesseract(@Header(value = KafkaHeaders.CORRELATION_ID, required = false) String correlationId, + @Header(value = "job_run_id", required = false) String jobRunId, + @Header(value = "tesseract_id", required = false) String tesseractId, + @Payload String payload + ) { + try { + log.info("Payload received from tesseract, metric result: {} for tesseract_id: {} , correlation_id: {}", payload, tesseractId, correlationId); + experimentMetricResultService.persistMetricResultForExperiment(payload, tesseractId); + } catch (Exception e) { + log.error("Error while processing metric result for tesseract_id: " + tesseractId, e); + MetricsUtils.tesseractMetrics("tesseract_metric_result_listener_failed") + .register(meterRegistry) + .increment(); + } + } +} diff --git a/litmus-core/src/main/java/com/navi/medici/mapper/ExperimentMapper.java b/litmus-core/src/main/java/com/navi/medici/mapper/ExperimentMapper.java index 9513bfd..9b05ab3 100644 --- a/litmus-core/src/main/java/com/navi/medici/mapper/ExperimentMapper.java +++ b/litmus-core/src/main/java/com/navi/medici/mapper/ExperimentMapper.java @@ -7,7 +7,7 @@ import com.navi.medici.entity.ExperimentAuditTrailEntity; import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentInfoEntity; import com.navi.medici.entity.ExperimentMetricMappingEntity; -import com.navi.medici.enums.ExperimentMetricType; +import com.navi.medici.entity.MetricEntity; import com.navi.medici.request.v1.AttachMetricToExperimentRequest; import com.navi.medici.response.DashboardExperimentResponse; import com.navi.medici.response.ExperimentResponse; @@ -28,26 +28,29 @@ import java.util.Set; public class ExperimentMapper { private final JacksonUtils jacksonUtils; - public DashboardExperimentResponse mapExperimentEntityToDashBoardExperimentResponse(ExperimentEntity experimentEntity) { - Optional primaryMetric = Objects.nonNull(experimentEntity.getExperimentMetricMappings()) - ? experimentEntity.getExperimentMetricMappings().stream() - .filter(mapping -> ExperimentMetricType.PRIMARY.equals(mapping.getExperimentMetricType())) - .findFirst() - : Optional.empty(); + public DashboardExperimentResponse mapExperimentEntityToDashBoardExperimentResponse(ExperimentEntity experimentEntity, Optional primaryMetric, ExperimentImpact experimentImpact) { + long testUsers = Objects.nonNull(experimentEntity.getExperimentInfo()) + ? experimentEntity.getExperimentInfo().getTestUsers() + : 0; + List variants = Objects.nonNull(experimentEntity.getVariants()) + ? jacksonUtils.stringToListObject(experimentEntity.getVariants(), VariantDefinition.class) + : List.of(); return DashboardExperimentResponse.builder() .experimentId(experimentEntity.getExperimentId()) .experimentName(experimentEntity.getName()) .experimentStatus(Objects.nonNull(experimentEntity.getExperimentInfo()) ? experimentEntity.getExperimentInfo().getExperimentStatus() : null) - .impact(ExperimentImpact.builder().build()) + .impact(experimentImpact) .createdBy(experimentEntity.getCreatedBy()) - .testUsers(Objects.nonNull(experimentEntity.getExperimentInfo()) - ? experimentEntity.getExperimentInfo().getTestUsers() - : 0) - .primaryMetric(primaryMetric.isPresent() - ? primaryMetric.get().getMetric().getMetricName() - : null) + .testUsers(testUsers) + .progressPercent( + (Objects.nonNull(experimentEntity.getExperimentInfo()) + && experimentEntity.getExperimentInfo().getExperimentMetadata().getSampleSizeRequired() > 0) + ? (double) testUsers / ((variants.size() > 0 ? variants.size() : 1) * experimentEntity.getExperimentInfo().getExperimentMetadata().getSampleSizeRequired()) + : 0 + ) + .primaryMetric(primaryMetric.map(MetricEntity::getMetricName).orElse(null)) .build(); } diff --git a/litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentMetricResultScheduler.java b/litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentMetricResultScheduler.java new file mode 100644 index 0000000..05c290f --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentMetricResultScheduler.java @@ -0,0 +1,38 @@ +package com.navi.medici.scheduler; + +import com.navi.medici.entity.ExperimentMetricMappingEntity; +import com.navi.medici.query.experimentmetricmapping.IExperimentMetricMappingQuery; +import com.navi.medici.service.experimentmetricresult.ExperimentMetricResultService; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import net.javacrumbs.shedlock.core.SchedulerLock; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class ExperimentMetricResultScheduler { + + private final IExperimentMetricMappingQuery experimentMetricMappingQuery; + private final ExperimentMetricResultService experimentMetricResultService; + private final MeterRegistry meterRegistry; + + @Value("${experiment.metric.fetch.limit.per.interval}") + private Integer experimentMetricFetchLimitPerInterval; + + @Scheduled(cron = "${experiment.metric.fetch.cron}") + @SchedulerLock(name = "ExperimentMetricResultScheduler_fetchExperimentMetricResult", lockAtMostForString = "PT10S") + public void fetchExperimentMetricResult() { + log.info("Scheduling query registration"); + List experimentMetricMappings = experimentMetricMappingQuery.findByIsJobRunLimitTo(false, experimentMetricFetchLimitPerInterval); + experimentMetricResultService.registerExperimentMetricQueries(experimentMetricMappings); + Gauge.builder("queries_scheduled_per_interval", experimentMetricMappings, List::size) + .register(meterRegistry); + } +} diff --git a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java index cf35d8d..e3d30c9 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java +++ b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java @@ -2,6 +2,7 @@ package com.navi.medici.service.experiment; import com.navi.medici.dto.Dropdown; import com.navi.medici.dto.ExperimentAuditTrailDTO; +import com.navi.medici.dto.ExperimentDataDTO; import com.navi.medici.request.v1.AttachMetricToExperimentRequest; import com.navi.medici.request.v1.CreateExperimentRequest; import com.navi.medici.request.v1.ExperimentSearchRequest; @@ -56,4 +57,6 @@ public interface ExperimentService { void restartExperiment(String experimentId, String emailId); List getExperimentAuditTrail(String experimentId); + + ExperimentDataDTO getExperimentStatsData(String experimentId); } diff --git a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java index 5954ee1..ba29f99 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java +++ b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java @@ -2,13 +2,18 @@ package com.navi.medici.service.experiment; import com.navi.medici.Constants; import com.navi.medici.config.LitmusCoreConfig; +import com.navi.medici.dto.DoughnutSectionDTO; import com.navi.medici.dto.Dropdown; import com.navi.medici.dto.ExperimentAuditTrailDTO; +import com.navi.medici.dto.ExperimentDataDTO; +import com.navi.medici.dto.ExperimentImpact; import com.navi.medici.dto.ExperimentMetadata; +import com.navi.medici.dto.GraphDTO; import com.navi.medici.entity.ExperimentAuditTrailEntity; import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentInfoEntity; import com.navi.medici.entity.ExperimentMetricMappingEntity; +import com.navi.medici.entity.ExperimentMetricResultEntity; import com.navi.medici.entity.MetricEntity; import com.navi.medici.entity.TeamEntity; import com.navi.medici.enums.ExperimentMetricType; @@ -38,13 +43,16 @@ import com.navi.medici.response.ExperimentResponse; import com.navi.medici.response.LitmusExperimentCollection; import com.navi.medici.response.PaginatedSearchResponse; import com.navi.medici.specification.ExperimentSpecification; +import com.navi.medici.stats.ChiSquaredTest; import com.navi.medici.stats.SampleSizeCalculator; import com.navi.medici.strategy.ActivationStrategy; +import com.navi.medici.util.DateTimeUtil; import com.navi.medici.util.JacksonUtils; import com.navi.medici.validator.ExperimentValidator; import com.navi.medici.variants.VariantDefinition; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.apache.commons.collections.map.HashedMap; import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -52,6 +60,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import javax.transaction.Transactional; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -59,6 +68,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -146,7 +156,7 @@ public class ExperimentServiceImpl implements ExperimentService { } ExperimentEntity savedExperiment = experimentQuery.save(experiment); saveAuditTrail(experiment.getExperimentId(), Constants.EXPERIMENT_CREATED, emailId); - return experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(savedExperiment); + return experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(savedExperiment, primaryMetric, ExperimentImpact.builder().build()); } @Override @@ -185,7 +195,15 @@ public class ExperimentServiceImpl implements ExperimentService { Page experimentEntities = experimentQuery.findAll(ExperimentSpecification.getExperiments(request), paging); return PaginatedSearchResponse.builder() .data(experimentEntities.stream() - .map(experimentMapper::mapExperimentEntityToDashBoardExperimentResponse) + .map(experimentEntity -> { + Optional primaryMetric = Objects.nonNull(experimentEntity.getExperimentMetricMappings()) + ? Optional.ofNullable(experimentEntity.getExperimentMetricMappings().stream() + .filter(mapping -> ExperimentMetricType.PRIMARY.equals(mapping.getExperimentMetricType())) + .findFirst().get().getMetric()) + : Optional.empty(); + ExperimentImpact experimentImpact = getExperimentImpact(experimentEntity, primaryMetric); + return experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(experimentEntity, primaryMetric, experimentImpact); + }) .collect(Collectors.toList())) .page(experimentEntities.getNumber() + 1) .size(experimentEntities.getSize()) @@ -193,6 +211,57 @@ public class ExperimentServiceImpl implements ExperimentService { .build(); } + private ExperimentImpact getExperimentImpact(ExperimentEntity experimentEntity, Optional primaryMetric) { + List experimentMetricResults = new ArrayList<>(); + if (primaryMetric.isPresent()) { + experimentMetricResults.addAll(experimentMetricResultQuery.findByExperimentAndMetricOrderByIdAsc(experimentEntity, primaryMetric.get())); + } + List variantNames = getVariantNamesForStats(experimentEntity); + Map converted = new HashMap<>(); + Map notConverted = new HashMap<>(); + variantNames.forEach(variant -> { + converted.put(variant, 0L); + notConverted.put(variant, 0L); + }); + experimentMetricResults.forEach(experimentMetricResult -> { + String variantName = experimentMetricResult.getVariantName(); + converted.put(variantName, converted.get(experimentMetricResult.getVariantName()) + experimentMetricResult.getConverted()); + notConverted.put(variantName, notConverted.get(experimentMetricResult.getVariantName()) + experimentMetricResult.getNotConverted()); + }); + ExperimentImpact experimentImpact = ExperimentImpact.builder() + .control(converted.get(Constants.CONTROL) != 0 + ? (double) (converted.get(Constants.CONTROL) * 100) / (converted.get(Constants.CONTROL) + notConverted.get(Constants.CONTROL)) + : 0) + .treatment(0.0) + .variantName(Constants.TREATMENT) + .build(); + if (Objects.nonNull(experimentEntity.getExperimentInfo())) { + variantNames.stream() + .filter(name -> !name.equals(Constants.CONTROL)) + .forEach(variant -> { + boolean flag = ChiSquaredTest.chiSquaredTest( + List.of(converted.get(Constants.CONTROL), converted.get(variant)), + List.of(notConverted.get(Constants.CONTROL), notConverted.get(variant)), + (double) 1 - (double) (experimentEntity.getExperimentInfo().getExperimentMetadata().getConfidenceInterval() / 100) + ); + double conversion = (converted.get(variant) == 0) + ? 0 + : (double) converted.get(variant) * 100 / (converted.get(variant) + notConverted.get(variant)); + if (conversion >= experimentImpact.getTreatment()) { + experimentImpact.setVariantName(variant); + experimentImpact.setTreatment(conversion); + if (flag) { + experimentImpact.setFlag(conversion > experimentImpact.getControl()); + } else { + experimentImpact.setFlag(null); + } + } + } + ); + } + return experimentImpact; + } + @Override public LitmusExperimentCollection fetchAllExperiments() { List experimentEntities = experimentQuery.findByEnabled(true); @@ -388,9 +457,9 @@ public class ExperimentServiceImpl implements ExperimentService { @Override public void attachMetric(String experimentId, AttachMetricToExperimentRequest request, String emailId) { - experimentValidator.validateAttachMetricRequest(request); - Optional metric = metricQuery.findByMetricName(request.getMetricName()); ExperimentEntity experiment = getExperimentEntityFromId(experimentId); + experimentValidator.validateAttachMetricRequest(request, experiment); + Optional metric = metricQuery.findByMetricName(request.getMetricName()); ExperimentMetricMappingEntity experimentMetricMapping = ExperimentMetricMappingEntity.builder() .experiment(experiment) .metric(metric.get()) @@ -492,4 +561,153 @@ public class ExperimentServiceImpl implements ExperimentService { experimentAuditTrail.sort(Comparator.comparing(ExperimentAuditTrailDTO::getCreatedAt)); return experimentAuditTrail; } + + @Override + public ExperimentDataDTO getExperimentStatsData(String experimentId) { + + ExperimentEntity experiment = getExperimentEntityFromId(experimentId); + List variantNames = getVariantNamesForStats(experiment); + List metricMappings = experimentMetricMappingQuery.findByExperiment(experiment); + List> table = getMetricStatsTableForExperiment(variantNames, metricMappings); + List populationDoughnut = getPopulationDoughnutForExperiment(experiment, variantNames); + List> populationGraph = getPopulationGraphForExperiment(experiment, litmusCoreConfig.getPopulationGraphDataDaysInterval(), variantNames); + + return ExperimentDataDTO.builder() + .metricTable(table) + .populationDoughnut(populationDoughnut) + .populationGraph(populationGraph) + .build(); + } + + private List> getPopulationGraphForExperiment(ExperimentEntity experiment, int populationGraphDataDaysInterval, List variantNames) { + Optional primaryMetricMapping = experiment.getExperimentMetricMappings().stream() + .filter(experimentMetricMapping -> ExperimentMetricType.PRIMARY.equals(experimentMetricMapping.getExperimentMetricType())) + .findFirst(); + if (primaryMetricMapping.isEmpty()) { + log.error("Primary metric not present for expriment: {}", experiment.getName()); + return List.of(); + } + MetricEntity primaryMetric = primaryMetricMapping.get().getMetric(); + List experimentResults = experiment.getExperimentMetricResults().stream() + .filter(experimentMetricResult -> experimentMetricResult.getMetric().getId().equals(primaryMetric.getId())) + .toList(); + + log.info("population graph data updated_after :{} , experiment results: {}", LocalDateTime.now().minusDays(populationGraphDataDaysInterval), experimentResults.toString()); + Map> variantPopulationValues = new HashMap<>(); + Map> variantPopulationTimestamps = new HashMap<>(); + populateGraphDataOfVariants(variantNames, experimentResults, variantPopulationValues, variantPopulationTimestamps); + List> populationGraph = new ArrayList<>(variantNames.stream() + .map(variantName -> GraphDTO.builder() + .name(variantName) + .values(variantPopulationValues.get(variantName)) + .timestamps(variantPopulationTimestamps.get(variantName)) + .build()) + .collect(Collectors.toList()) + ); + if (variantNames.size() > 2) { + return populationGraph.stream() + .filter(graphDTO -> !graphDTO.getName().equals(Constants.TREATMENT)) + .collect(Collectors.toList()); + } else { + return populationGraph; + } + } + + private void populateGraphDataOfVariants(List variantNames, List experimentResults, Map> variantPopulationValues, Map> variantPopulationTimestamps) { + variantNames.forEach(variantName -> { + variantPopulationValues.put(variantName, List.of()); + variantPopulationTimestamps.put(variantName, List.of()); + }); + experimentResults.forEach(experimentResult -> { + String variantName = experimentResult.getVariantName(); + List population = new ArrayList<>(variantPopulationValues.get(variantName)); + List timestamp = new ArrayList<>(variantPopulationTimestamps.get(variantName)); + population.add(experimentResult.getTotalUsers()); + timestamp.add(DateTimeUtil.convertDateTimeToDefaultZone(experimentResult.getTimestamp())); + variantPopulationValues.put(variantName, population); + variantPopulationTimestamps.put(variantName, timestamp); + }); + } + + private List getPopulationDoughnutForExperiment(ExperimentEntity experiment, List variantNames) { + Optional primaryMetricMapping = experiment.getExperimentMetricMappings().stream() + .filter(experimentMetricMapping -> ExperimentMetricType.PRIMARY.equals(experimentMetricMapping.getExperimentMetricType())) + .findFirst(); + if (primaryMetricMapping.isEmpty()) { + log.error("Primary metric not present for expriment: {}", experiment.getName()); + return List.of(); + } + MetricEntity primaryMetric = primaryMetricMapping.get().getMetric(); + List experimentResults = experiment.getExperimentMetricResults().stream() + .filter(experimentMetricResult -> experimentMetricResult.getMetric().getId().equals(primaryMetric.getId())) + .toList(); + Map variantPopulation = new HashMap<>(); + variantNames.forEach(variantName -> { + variantPopulation.put(variantName, 0L); + }); + experimentResults.forEach(experimentResult -> { + String variantName = experimentResult.getVariantName(); + variantPopulation.put(variantName, variantPopulation.get(variantName) + experimentResult.getTotalUsers()); + }); + List populationDoughnut = new ArrayList<>(variantNames.stream() + .map(variantName -> DoughnutSectionDTO.builder() + .key(variantName) + .value(variantPopulation.get(variantName)) + .build()) + .collect(Collectors.toList()) + ); + if (variantNames.size() > 2) { + return populationDoughnut.stream() + .filter(doughnut -> !doughnut.getKey().equals(Constants.TREATMENT)) + .collect(Collectors.toList()); + } else { + return populationDoughnut; + } + } + + private List> getMetricStatsTableForExperiment(List variantNames, List metricMappings) { + List> table = new ArrayList<>(); + metricMappings.forEach(metricMapping -> { + addMetricDataToTable(variantNames, table, metricMapping); + }); + return table; + } + + private void addMetricDataToTable(List variantNames, List> table, ExperimentMetricMappingEntity metricMapping) { + List metricResults = experimentMetricResultQuery.findByExperimentAndMetricOrderByIdAsc(metricMapping.getExperiment(), metricMapping.getMetric()); + Map converted = new HashedMap(); + Map notConverted = new HashMap(); + variantNames.forEach(variantName -> converted.put(variantName, 0L)); + variantNames.forEach(variantName -> notConverted.put(variantName, 0L)); + metricResults.forEach(metricResult -> { + String variantName = metricResult.getVariantName(); + converted.put(variantName, converted.get(variantName) + metricResult.getConverted()); + notConverted.put(variantName, notConverted.get(variantName) + metricResult.getNotConverted()); + }); + Map metricResult = new HashMap<>(); + metricResult.put("metricName", metricMapping.getMetric().getMetricName()); + metricResult.put("experimentMetricType", metricMapping.getExperimentMetricType()); + variantNames.forEach(variantName -> { + if (converted.get(variantName) == 0) { + metricResult.put(variantName, 0); + } else { + metricResult.put(variantName, (double) (converted.get(variantName) * 100) / (converted.get(variantName) + notConverted.get(variantName))); + } + }); + if (variantNames.size() > 2) { + metricResult.remove(Constants.TREATMENT); + } + table.add(metricResult); + } + + private List getVariantNamesForStats(ExperimentEntity experiment) { + List variantNames = new ArrayList<>(); + variantNames.add(Constants.CONTROL); + variantNames.add(Constants.TREATMENT); + if (Objects.nonNull(experiment.getVariants())) { + List variants = jacksonUtils.stringToListObject(experiment.getVariants(), VariantDefinition.class); + variants.forEach(variant -> variantNames.add(variant.getName())); + } + return variantNames; + } } diff --git a/litmus-core/src/main/java/com/navi/medici/service/experimentmetricresult/ExperimentMetricResultService.java b/litmus-core/src/main/java/com/navi/medici/service/experimentmetricresult/ExperimentMetricResultService.java new file mode 100644 index 0000000..2818a83 --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/service/experimentmetricresult/ExperimentMetricResultService.java @@ -0,0 +1,14 @@ +package com.navi.medici.service.experimentmetricresult; + +import com.navi.medici.entity.ExperimentMetricMappingEntity; +import com.navi.medici.tesseract.response.TesseractIdStatusResponse; + +import java.util.List; + +public interface ExperimentMetricResultService { + void persistMetricResultForExperiment(String queryResponseFromTesseract, String tesseractId); + + void registerExperimentMetricQueries(List experimentMetricMappings); + + void updateStatusOfTesseractId(TesseractIdStatusResponse response); +} diff --git a/litmus-core/src/main/java/com/navi/medici/service/experimentmetricresult/ExperimentMetricResultServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/experimentmetricresult/ExperimentMetricResultServiceImpl.java new file mode 100644 index 0000000..84a425d --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/service/experimentmetricresult/ExperimentMetricResultServiceImpl.java @@ -0,0 +1,194 @@ +package com.navi.medici.service.experimentmetricresult; + +import com.navi.medici.Constants; +import com.navi.medici.config.LitmusCoreConfig; +import com.navi.medici.dto.MetricResult; +import com.navi.medici.entity.ExperimentEntity; +import com.navi.medici.entity.ExperimentInfoEntity; +import com.navi.medici.entity.ExperimentMetricMappingEntity; +import com.navi.medici.entity.ExperimentMetricResultEntity; +import com.navi.medici.entity.MetricEntity; +import com.navi.medici.entity.TesseractIdStatusEntity; +import com.navi.medici.enums.ExperimentMetricType; +import com.navi.medici.metrics.MetricsUtils; +import com.navi.medici.query.experiment.IExperimentQuery; +import com.navi.medici.query.experimentmetricmapping.IExperimentMetricMappingQuery; +import com.navi.medici.query.experimentmetricresult.IExperimentMetricResultQuery; +import com.navi.medici.query.metric.IMetricQuery; +import com.navi.medici.query.tesseractidstatus.ITesseractIdStatusQuery; +import com.navi.medici.service.queryprocessing.TesseractQueryProcessingService; +import com.navi.medici.tesseract.TesseractClient; +import com.navi.medici.tesseract.response.TesseractIdStatusResponse; +import com.navi.medici.tesseract.response.TesseractQueryRegisterResponse; +import com.navi.medici.util.DateTimeUtil; +import com.navi.medici.util.JacksonUtils; +import freemarker.template.TemplateException; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@Service +@Log4j2 +@RequiredArgsConstructor +public class ExperimentMetricResultServiceImpl implements ExperimentMetricResultService { + private final IExperimentMetricResultQuery experimentMetricResultQuery; + private final IExperimentMetricMappingQuery experimentMetricMappingQuery; + private final IExperimentQuery experimentQuery; + private final IMetricQuery metricQuery; + private final JacksonUtils jacksonUtils; + private final TesseractClient tesseractClient; + private final TesseractQueryProcessingService tesseractQueryProcessingService; + private final ITesseractIdStatusQuery tesseractIdStatusQuery; + + private final MeterRegistry meterRegistry; + private final LitmusCoreConfig litmusCoreConfig; + + @Override + @Transactional + public void persistMetricResultForExperiment(String queryResponseFromTesseract, String tesseractId) { + MetricResult metricResult = jacksonUtils.stringToObject(queryResponseFromTesseract, MetricResult.class); + Optional experimentOpt = experimentQuery.findById(metricResult.getExperimentId()); + Optional metric = metricQuery.findById(metricResult.getMetricId()); + if (experimentOpt.isEmpty() || metric.isEmpty()) { + log.error("{} with id: {} not present", experimentOpt.isEmpty() ? "experiment" : "metric", metricResult.getExperimentId()); + return; + } + ExperimentEntity experiment = experimentOpt.get(); + ExperimentMetricResultEntity experimentMetricResult = buildExperimentMetricResultEntity(tesseractId, metricResult, metric, experiment); + Optional experimentMetricMapping = experimentMetricMappingQuery.findByExperimentAndMetric(experiment, metric.get()); + if (ExperimentMetricType.PRIMARY.equals(experimentMetricMapping.get().getExperimentMetricType())) { + ExperimentInfoEntity experimentInfo = experiment.getExperimentInfo(); + experimentInfo.setTestUsers(experimentInfo.getTestUsers() + metricResult.getTotalUsers()); + experiment.setExperimentInfo(experimentInfo); + + log.info("saving experiments with updated test users: {}", experiment.getExperimentInfo().getTestUsers()); + experimentQuery.save(experiment); + } + experimentMetricResultQuery.save(experimentMetricResult); + } + + private static ExperimentMetricResultEntity buildExperimentMetricResultEntity(String tesseractId, MetricResult metricResult, Optional metric, ExperimentEntity experiment) { + return ExperimentMetricResultEntity.builder() + .experiment(experiment) + .metric(metric.get()) + .tesseractId(tesseractId) + .variantName(metricResult.getVariantName()) + .converted(metricResult.getConverted()) + .notConverted(metricResult.getNotConverted()) + .totalUsers(metricResult.getTotalUsers()) + .timestamp(metricResult.getTimestasmp()) + .build(); + } + + @Override + public void registerExperimentMetricQueries(List experimentMetricMappings) { + setJobsToTrueForMappings(experimentMetricMappings); + if (experimentMetricMappings.size() > 0) { + experimentMetricMappings.forEach(experimentMetricMapping -> { + ExperimentEntity experiment = experimentMetricMapping.getExperiment(); + MetricEntity metric = experimentMetricMapping.getMetric(); + log.info("registering query for metric: {} mapped to experiment: {}", metric.getMetricName(), experiment.getName()); + Map variableMap = new HashMap<>(); + if (Objects.nonNull(experimentMetricMapping.getQueryVariableMapping())) { + variableMap = experimentMetricMapping.getQueryVariableMapping(); + } + variableMap.putAll(getNecessaryVariableMappingForQuery(experiment, metric)); + try { + String processedQuery = tesseractQueryProcessingService.processMetricQuery(metric, variableMap); + TesseractQueryRegisterResponse tesseractQueryRegisterResponse = tesseractClient.register().apply(tesseractClient.getTesseractQueryRegisterRequest(processedQuery)); + if (Objects.nonNull(tesseractQueryRegisterResponse)) { + persistTesseractIdStatus(experiment, metric, tesseractQueryRegisterResponse); + } else { + log.error("Unable to register query for metric: {} mapped to experiment: {}", metric.getMetricName(), experiment.getName()); + MetricsUtils.tesseractMetrics("query_registration_failures") + .tag("experiment_name", experiment.getName()) + .tag("metric_name", metric.getMetricName()) + .register(meterRegistry) + .increment(); + } + } catch (TemplateException | IOException e) { + MetricsUtils.tesseractMetrics("metric_query_resolve_failure") + .tag("experiment_name", experiment.getName()) + .tag("metric_name", metric.getMetricName()) + .register(meterRegistry) + .increment(); + throw new RuntimeException(String.format("Failed to process metric query: %s", metricQuery), e); + } + }); + } else { + setJobsToFalse(); + } + } + + private void persistTesseractIdStatus(ExperimentEntity experiment, MetricEntity metric, TesseractQueryRegisterResponse tesseractQueryRegisterResponse) { + TesseractIdStatusEntity tesseractIdStatus = TesseractIdStatusEntity.builder() + .experiment(experiment) + .metric(metric) + .tesseractId(tesseractQueryRegisterResponse.getTesseractId()) + .build(); + tesseractClient.statusUpdate(tesseractQueryRegisterResponse.getTesseractId()) + .ifPresent(response -> tesseractIdStatus.setStatus(response.getStatus())); + tesseractIdStatusQuery.save(tesseractIdStatus); + } + + private void setJobsToTrueForMappings(List experimentMetricMappings) { + experimentMetricMappings.forEach(experimentMetricMapping -> { + experimentMetricMapping.setJobRun(true); + log.info("saving experimentMetricMapping for id: {}", experimentMetricMapping.getId()); + experimentMetricMappingQuery.save(experimentMetricMapping); + }); + } + + private void setJobsToFalse() { + + log.info("setting all experiment-metric jobs to false"); + List experimentMetricMappings; + experimentMetricMappings = experimentMetricMappingQuery.findAll(); + experimentMetricMappings.forEach(experimentMetricMapping -> { + experimentMetricMapping.setJobRun(false); + experimentMetricMappingQuery.save(experimentMetricMapping); + }); + } + + @Override + public void updateStatusOfTesseractId(TesseractIdStatusResponse response) { + Optional tesseractIdStatus = tesseractIdStatusQuery.findByTesseractId(response.getTesseractId()); + if (tesseractIdStatus.isEmpty()) { + log.error("tesseractId :{} does not exist when updating status", response.getTesseractId()); + return; + } + tesseractIdStatus.get().setStatus(response.getStatus()); + tesseractIdStatusQuery.save(tesseractIdStatus.get()); + } + + private Map getNecessaryVariableMappingForQuery(ExperimentEntity experiment, MetricEntity metric) { + Map variableMap = new HashMap<>(); + variableMap.put(Constants.EXPERIMENT_NAME, experiment.getName()); + variableMap.put(Constants.EXPERIMENT_ID, experiment.getId()); + variableMap.put(Constants.METRIC_ID, metric.getId()); + variableMap.put(Constants.EXPERIMENT_CREATED_AT, DateTimeUtil.convertToSpacedISODateTime(experiment.getCreatedAt())); + LocalDateTime scheduleTime = LocalDateTime.now(); + variableMap.put(Constants.SCHEDULE_TIME, DateTimeUtil.convertToSpacedISODateTime(scheduleTime)); + variableMap.put(Constants.END_TIME, DateTimeUtil.convertToSpacedISODateTime(scheduleTime.minusHours(litmusCoreConfig.getQueryScheduleAndEndTimeDiff()))); + List experimentResults = experimentMetricResultQuery.findByExperiment(experiment); + if (experimentResults.isEmpty()) { + variableMap.put(Constants.START_TIME, DateTimeUtil.convertToSpacedISODateTime(scheduleTime.minusDays(1))); + } else { + List experimentMetricResults = experimentResults.stream().toList(); + variableMap.put(Constants.START_TIME, DateTimeUtil.convertToSpacedISODateTime(experimentMetricResults.get(experimentMetricResults.size() - 1) + .getTimestamp())); + } + variableMap.put(Constants.DAY_INTERVAL_FOR_TOTAL_USERS, litmusCoreConfig.getQueryDayIntervalForTotalUsers()); + return variableMap; + } +} diff --git a/litmus-core/src/main/java/com/navi/medici/service/metric/MetricServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/metric/MetricServiceImpl.java index d6b31d5..4117f24 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/metric/MetricServiceImpl.java +++ b/litmus-core/src/main/java/com/navi/medici/service/metric/MetricServiceImpl.java @@ -9,6 +9,7 @@ import com.navi.medici.request.v1.CreateMetricRequest; import com.navi.medici.request.v1.MetricSearchRequest; import com.navi.medici.response.MetricResponse; import com.navi.medici.response.PaginatedSearchResponse; +import com.navi.medici.service.queryprocessing.TesseractQueryProcessingService; import com.navi.medici.specification.MetricSpecification; import com.navi.medici.validator.MetricValidator; import lombok.RequiredArgsConstructor; @@ -34,6 +35,7 @@ public class MetricServiceImpl implements MetricService { private final MetricValidator metricRequestValidator; private final IExperimentMetricMappingQuery experimentMetricMappingQuery; + private final TesseractQueryProcessingService tesseractQueryProcessingService; @Override @Transactional @@ -49,6 +51,12 @@ public class MetricServiceImpl implements MetricService { .createdBy(emailId) .athenaQuery(createMetricRequest.getAthenaQuery()) .build(); + try { + List queryVariables = tesseractQueryProcessingService.getVariablesFromMetricQuery(metric); + metric.setQueryVariables(queryVariables); + } catch (Exception e) { + log.error("Unable to resolve variables for metric: {}", metric.getMetricName()); + } MetricEntity createdMetric = metricQuery.save(metric); return metricMapper.mapMetricEntityToMetricResponse(createdMetric, 0); } diff --git a/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingService.java b/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingService.java new file mode 100644 index 0000000..e09d5ae --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingService.java @@ -0,0 +1,14 @@ +package com.navi.medici.service.queryprocessing; + +import com.navi.medici.entity.MetricEntity; +import freemarker.template.TemplateException; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public interface TesseractQueryProcessingService { + List getVariablesFromMetricQuery(MetricEntity metric) throws IOException; + + String processMetricQuery(MetricEntity metric, Map variableMap) throws TemplateException, IOException; +} diff --git a/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingServiceImpl.java new file mode 100644 index 0000000..7b95f78 --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingServiceImpl.java @@ -0,0 +1,27 @@ +package com.navi.medici.service.queryprocessing; + +import com.navi.medici.entity.MetricEntity; +import com.navi.medici.freemarker.FreeMarkerUtil; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +@Service +@Log4j2 +public class TesseractQueryProcessingServiceImpl implements TesseractQueryProcessingService { + @SneakyThrows + @Override + public List getVariablesFromMetricQuery(MetricEntity metric) { + return FreeMarkerUtil.getVariablesFromFreeMarkerMessage(metric.getMetricName(), metric.getMetricId(), metric.getAthenaQuery()); + } + + @SneakyThrows + @Override + public String processMetricQuery(MetricEntity metric, Map variableMap) { + return FreeMarkerUtil.processFreeMarkerMessage(metric.getMetricName(), metric.getMetricId(), metric.getAthenaQuery(), variableMap); + } + +} diff --git a/litmus-core/src/main/java/com/navi/medici/tesseract/TesseractClient.java b/litmus-core/src/main/java/com/navi/medici/tesseract/TesseractClient.java new file mode 100644 index 0000000..3d7ab7d --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/tesseract/TesseractClient.java @@ -0,0 +1,236 @@ +package com.navi.medici.tesseract; + +import com.navi.medici.config.LitmusCoreConfig; +import com.navi.medici.handler.RetryableException; +import com.navi.medici.metrics.MetricsUtils; +import com.navi.medici.tesseract.request.SyncType; +import com.navi.medici.tesseract.request.TesseractCSVRegisterRequest; +import com.navi.medici.tesseract.request.TesseractQueryRegisterRequest; +import com.navi.medici.tesseract.request.TesseractQueryUpdateRequest; +import com.navi.medici.tesseract.response.TesseractIdStatusResponse; +import com.navi.medici.tesseract.response.TesseractQueryDeleteResponse; +import com.navi.medici.tesseract.response.TesseractQueryRegisterResponse; +import com.navi.medici.tesseract.response.TesseractQueryStatsResponse; +import com.navi.medici.tesseract.response.TesseractQueryUpdateResponse; +import io.github.resilience4j.core.IntervalFunction; +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.PostConstruct; +import java.util.Optional; +import java.util.function.Function; + +@Component +@RequiredArgsConstructor +@Log4j2 +public class TesseractClient { + private final RestTemplate restTemplate; + private final LitmusCoreConfig litmusCoreConfig; + private RetryConfig retryConfig; + private final MeterRegistry meterRegistry; + + @PostConstruct + public void postConstruct() { + // dp has limit of 2 req per sec + IntervalFunction intervalFn = + IntervalFunction.ofExponentialRandomBackoff(litmusCoreConfig.getIntervalFunctionInitialInterval() + , litmusCoreConfig.getIntervalFunctionMultiplier() + , litmusCoreConfig.getIntervalFunctionMaxInterval()); + this.retryConfig = RetryConfig.custom() + .maxAttempts(litmusCoreConfig.getTesseractRegisterJobMaxRetries()) + .intervalFunction(intervalFn) + .retryExceptions(RetryableException.class) + .build(); + + } + + + @Timed(value = "tesseract_client_request_register", percentiles = {0.90, 0.95, 0.99}) + public Function register() { + Retry retry = Retry.of("tesseract_register", retryConfig); + return Retry.decorateFunction(retry, getTesseractQueryRegisterResponse()); + } + + private Function getTesseractQueryRegisterResponse() { + return tesseractQueryRegisterRequest -> { + String url = litmusCoreConfig.getTesseractHost() + litmusCoreConfig.getTesseractRegisterPath(); + try { + ResponseEntity response = restTemplate + .exchange(url, HttpMethod.POST, createRegisterRequestPayload(tesseractQueryRegisterRequest), + TesseractQueryRegisterResponse.class + ); + + log.info("tesseract id: {} for query: {}", response.getBody().getTesseractId(), + tesseractQueryRegisterRequest.getSourceConfig().getAttributes().getQueryText() + ); + MetricsUtils.tesseractMetrics("tesseract_query_registration_success") + .register(meterRegistry) + .increment(); + return response.getBody(); + } catch (HttpClientErrorException | HttpServerErrorException e) { + log.error("tesseract query registration failed. status: {}", e.getRawStatusCode(), e); + MetricsUtils.tesseractMetrics("tesseract_query_registration_failures") + .tag("status_code", String.valueOf(e.getRawStatusCode())) + .register(meterRegistry) + .increment(); + if (e.getStatusCode().equals(HttpStatus.TOO_MANY_REQUESTS)) { + log.error( + "retrying the job for the query : {}", + tesseractQueryRegisterRequest.getSourceConfig().getAttributes().getQueryText() + ); + //publishing metrics for rate limit hit + MetricsUtils.tesseractMetrics("tesseract_too_many_hits_count") + .register(meterRegistry) + .increment(); + throw new RetryableException(e.getMessage()); + } + } + return null; + }; + } + + @Timed(value = "tesseract_client_request_register_csv", percentiles = {0.90, 0.95, 0.99}) + public Optional registerCSV(TesseractCSVRegisterRequest tesseractQueryRegisterRequest) { + String url = litmusCoreConfig.getTesseractHost() + litmusCoreConfig.getTesseractRegisterPath(); + try { + ResponseEntity response = restTemplate + .exchange( + url, HttpMethod.POST, createRegisterRequestPayload(tesseractQueryRegisterRequest), + TesseractQueryRegisterResponse.class + ); + + return Optional.ofNullable(response.getBody()); + } catch (HttpClientErrorException | HttpServerErrorException e) { + log.error("tesseract csv registration failed. status: {}", e.getRawStatusCode(), e); + + MetricsUtils.tesseractMetrics("tesseract_csv_registration_failures") + .register(meterRegistry) + .increment(); + } + + return Optional.empty(); + } + + private HttpEntity createRegisterRequestPayload(Object registerRequest) { + HttpHeaders headers = new HttpHeaders(); + headers.add("X-TEAM-NAME", litmusCoreConfig.getTesserractTeamName()); + + return new HttpEntity<>( + registerRequest, + headers + ); + } + + @Timed(value = "tesseract_client_update_query", percentiles = {0.90, 0.95, 0.99}) + public Optional update( + String tesseractId, + TesseractQueryUpdateRequest tesseractQueryUpdateRequest + ) { + String url = litmusCoreConfig.getTesseractHost() + String.format(litmusCoreConfig.getTesseractUpdatePath(), tesseractId); + try { + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, + new HttpEntity<>(tesseractQueryUpdateRequest, new HttpHeaders()), TesseractQueryUpdateResponse.class + ); + + return Optional.ofNullable(response.getBody()); + } catch (HttpClientErrorException | HttpServerErrorException e) { + log.error("tesseract query update failed. tesseract_id: {}, status: {}", + tesseractId, e.getRawStatusCode(), e + ); + } + return Optional.empty(); + } + + public Optional delete(String tesseractId) { + String url = litmusCoreConfig.getTesseractHost() + String.format(litmusCoreConfig.getTesseractUpdatePath(), tesseractId); + try { + ResponseEntity response = restTemplate.exchange(url, HttpMethod.DELETE, + new HttpEntity<>(new HttpHeaders()), TesseractQueryDeleteResponse.class + ); + + return Optional.ofNullable(response.getBody()); + } catch (HttpClientErrorException | HttpServerErrorException e) { + log.error("tesseract query delete failed. tesseract_id: {}, status: {}", + tesseractId, e.getRawStatusCode(), e + ); + MetricsUtils.tesseractMetrics("tesseract_query_delete_failure") + .register(meterRegistry) + .increment(); + } + return Optional.empty(); + } + + @Timed(value = "tesseract_client_status_update", percentiles = {0.90, 0.95, 0.99}) + public Optional statusUpdate(String tesseractId) { + String url = litmusCoreConfig.getTesseractHost() + String.format(litmusCoreConfig.getTesseractStatusPath(), tesseractId); + try { + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, + new HttpEntity<>(new HttpHeaders()), TesseractIdStatusResponse.class + ); + + return Optional.ofNullable(response.getBody()); + } catch (HttpClientErrorException | HttpServerErrorException e) { + log.error("tesseract query status update failed. tesseract_id: {}, status: {}", + tesseractId, e.getRawStatusCode(), e + ); + MetricsUtils.tesseractMetrics("tesseract_query_status_update_failure") + .register(meterRegistry) + .increment(); + } + return Optional.empty(); + } + + @Timed(value = "tesseract_client_stats", percentiles = {0.90, 0.95, 0.99}) + public Optional stats(String tesseractId) { + String url = litmusCoreConfig.getTesseractHost() + String.format(litmusCoreConfig.getTesseractStatsPath(), tesseractId); + try { + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, + new HttpEntity<>(new HttpHeaders()), TesseractQueryStatsResponse.class + ); + + return Optional.ofNullable(response.getBody()); + } catch (HttpClientErrorException | HttpServerErrorException e) { + log.error("tesseract query stats failed. tesseract_id: {}, status: {}", + tesseractId, e.getRawStatusCode(), e + ); + MetricsUtils.tesseractMetrics("tesseract_query_stats_failure") + .register(meterRegistry) + .increment(); + } + return Optional.empty(); + } + + public TesseractQueryRegisterRequest getTesseractQueryRegisterRequest(String query) { + return TesseractQueryRegisterRequest.builder() + .sourceConfig(TesseractQueryRegisterRequest.Source.builder() + .sourceType("query") + .attributes(TesseractQueryRegisterRequest.Attributes.builder() + .queryText(query) + .build()) + .build()) + .destinationConfig(TesseractQueryRegisterRequest.Destination.builder() + .destinationType("kafka") + .attributes(TesseractQueryRegisterRequest.Attributes.builder() + .kafkaTopic(litmusCoreConfig.getExperimentMetricResultTopic()) + .clusterName(litmusCoreConfig.getTesseractClusterName()) + .build()) + .build()) + .syncType(SyncType.ADHOC) + .team(litmusCoreConfig.getTesserractTeamName()) + .transitionCallback(litmusCoreConfig.getLitmusCoreBaseUrl() + "/litmus-core/v2/experiments/tesseract-id-status") + .build(); + } +} \ No newline at end of file diff --git a/litmus-core/src/main/java/com/navi/medici/util/DateTimeUtil.java b/litmus-core/src/main/java/com/navi/medici/util/DateTimeUtil.java index 4e5e8d7..887d36b 100644 --- a/litmus-core/src/main/java/com/navi/medici/util/DateTimeUtil.java +++ b/litmus-core/src/main/java/com/navi/medici/util/DateTimeUtil.java @@ -2,6 +2,7 @@ package com.navi.medici.util; import java.time.LocalDateTime; import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.TimeZone; public class DateTimeUtil { @@ -10,4 +11,8 @@ public class DateTimeUtil { public static LocalDateTime convertDateTimeToDefaultZone(LocalDateTime dateTime) { return dateTime.atZone(ZoneId.of(String.valueOf(TimeZone.getTimeZone(ZoneId.systemDefault()).toZoneId()))).withZoneSameInstant(DEFAULT_ZONE_ID).toLocalDateTime(); } + + public static String convertToSpacedISODateTime(LocalDateTime dateTime) { + return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } } diff --git a/litmus-core/src/main/java/com/navi/medici/validator/ExperimentValidator.java b/litmus-core/src/main/java/com/navi/medici/validator/ExperimentValidator.java index ab0151b..8e319fa 100644 --- a/litmus-core/src/main/java/com/navi/medici/validator/ExperimentValidator.java +++ b/litmus-core/src/main/java/com/navi/medici/validator/ExperimentValidator.java @@ -1,9 +1,14 @@ package com.navi.medici.validator; +import com.navi.medici.entity.ExperimentEntity; +import com.navi.medici.entity.ExperimentMetricMappingEntity; import com.navi.medici.entity.MetricEntity; import com.navi.medici.exceptions.BadRequestException; +import com.navi.medici.exceptions.DuplicateMetricMappingToExperimentException; +import com.navi.medici.exceptions.StatisticalException; import com.navi.medici.exceptions.VariantWeightSumNotHundredException; import com.navi.medici.query.experiment.IExperimentQuery; +import com.navi.medici.query.experimentmetricmapping.IExperimentMetricMappingQuery; import com.navi.medici.query.metric.IMetricQuery; import com.navi.medici.request.v1.AttachMetricToExperimentRequest; import com.navi.medici.request.v1.CreateExperimentRequest; @@ -17,6 +22,8 @@ import org.springframework.stereotype.Component; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; @Data @Builder @@ -25,6 +32,7 @@ import java.util.Optional; public class ExperimentValidator { private final IExperimentQuery experimentQuery; private final IMetricQuery metricQuery; + private final IExperimentMetricMappingQuery experimentMetricMappingQuery; public void validateCreateExperimentRequest(CreateExperimentRequest request) { if (StringUtils.isBlank(request.getExperimentName()) || request.getExperimentName().contains(" ")) { @@ -46,9 +54,15 @@ public class ExperimentValidator { throw new VariantWeightSumNotHundredException("Sum of weights of variants should be 100"); } } + if (request.getConfidenceInterval() < 95.0) { + throw new StatisticalException("Confidence interval should be greater than "); + } + if (request.getPrimaryMetric().equals(request.getSecondaryMetric())) { + throw new DuplicateMetricMappingToExperimentException(request.getSecondaryMetric() + " is already mapped to the experiment"); + } } - public void validateAttachMetricRequest(AttachMetricToExperimentRequest request) { + public void validateAttachMetricRequest(AttachMetricToExperimentRequest request, ExperimentEntity experiment) { if (StringUtils.isBlank(request.getMetricName())) { throw new BadRequestException("Metric Name should not be empty or null"); } @@ -59,6 +73,12 @@ public class ExperimentValidator { if (metric.isEmpty()) { throw new BadRequestException("Metric With name: " + request.getMetricName() + " does not exist"); } + Set experimentMetricMappings = experiment.getExperimentMetricMappings(); + if (!experimentMetricMappings.stream() + .filter(experimentMetricMapping -> experimentMetricMapping.getMetric().getMetricName().equals(request.getMetricName())) + .collect(Collectors.toList()).isEmpty()) { + throw new DuplicateMetricMappingToExperimentException(request.getMetricName() + " is already mapped to the experiment"); + } } public void validateAttachVariantsRequest(List variantDefinitions) { diff --git a/litmus-core/src/main/resources/application.properties b/litmus-core/src/main/resources/application.properties index 74992ea..82183cf 100644 --- a/litmus-core/src/main/resources/application.properties +++ b/litmus-core/src/main/resources/application.properties @@ -1,7 +1,7 @@ server.servlet.context-path=/litmus-core spring.main.banner-mode=off -spring.application.name = litmus-core -server.port = ${PORT:12000} +spring.application.name=litmus-core +server.port=${PORT:12000} spring.datasource.hikari.maximum-pool-size=${DB_POOL_MAX_SIZE:2} spring.datasource.hikari.minimum-idle=${DB_POOL_MIN_IDLE:1} spring.datasource.hikari.idle-timeout=${DB_POOL_IDLE_TIMEOUT_IN_MS:30000} @@ -14,33 +14,48 @@ spring.jpa.hibernate.ddl-auto=none #spring.jpa.properties.hibernate.show_sql=true #spring.jpa.properties.hibernate.use_sql_comments=true #spring.jpa.properties.hibernate.format_sql=true - management.server.port=4001 management.endpoints.web.exposure.include=prometheus,health,info,metric,heapdump,threaddump server.tomcat.mbeanregistry.enabled=true spring.jmx.enabled=true management.metrics.kafka.consumer.enabled=true management.metrics.kafka.producer.enabled=true - kafka.servers=${KAFKA_SERVER:localhost:9092} audit.kafka.servers=${AUDIT_KAFKA_SERVER:localhost:9092} kafka.auditlog.topic=${AUDIT_LOG_TOPIC:audit-logs} kms.base-url=${KMS_BASE_URL:http://google.com} - - redis.host=${REDIS_HOST:127.0.0.1} redis.port=6379 redis.expected.insertions=99999 redis.false.probability=0.001 - segment.s3.bucket=${SEGMENT_S3_BUCKET:navi-test} s3.enabled=${S3_ENABLED:false} - springdoc.swagger-ui.path=/swagger-ui.html - spring.servlet.multipart.max-file-size=-1 spring.servlet.multipart.max-request-size=-1 - -default.polling.time.seconds= ${DEFAULT_POLLING_TIME_SECONDS:300} -litmus.portal.base.url = ${LITMUS_PORTAL_BASE_URL:http://localhost:4000} -litmus.portal.allowed.methods = ${LITMUS_PORTAL_ALLOWED_METHODS:GET,POST,PUT} \ No newline at end of file +default.polling.time.seconds=${DEFAULT_POLLING_TIME_SECONDS:300} +#litmus portal +litmus.portal.base.url=${LITMUS_PORTAL_BASE_URL:http://localhost:4000} +litmus.portal.allowed.methods=${LITMUS_PORTAL_ALLOWED_METHODS:GET,POST,PUT} +#tesseract +tesseract.register.job.max.retries=${TESSERACT_REGISTER_JOB_MAX_RETRIES:3} +tesseract.host=${TESSERACT_HOST:https://tesseract-service.np.navi-tech.in} +tesseract.register.path=${TESSERACT_REGISTER_PATH:/onboard/register} +tesseract.team.name=${TESSERACT_TEAM:litmus} +tesseract.update.path=${TESSERACT_UPDATE_PATH:/onboard/update/%s} +tesseract.status.path=${TESSERACT_STATUS_PATH:/onboard/status/%s} +tesseract.stats.path=${TESSERACT_STATS_PATH:/onboard/stats/%s} +tesseract.litmus.cluster.name=${TESSERACT_LITMUS_CLUSTER_NAME:dev-kafka} +litmus.core.base.url=${LITMUS_CORE_BASE_URL:http://localhost:12000} +tesseract.interval.function.initial.interval=${TESSERACT_INTERVAL_FUNCTION_INITIAL_INTERVAL:60000} +tesseract.interval.function.max.interval=${TESSERACT_INTERVAL_FUNCTION_MAX_INTERVAL:100000} +tesseract.interval.function.multiplier=${TESSERACT_INTERVAL_FUNCTION_MULTIPLIER:5} +#kafka +kafka.experiment.metric.result.topic=${KAFKA_EXPERIMENT_METRIC_RESULT_TOPIC:common-litmus-experiment-metric-result-topic} +kafka.litmus.consumer.group=${KAFKA_LITMUS_CONSUMER_GROUP: dev-kafka} +#metrics +experiment.metric.fetch.cron=${EXPERIMENT_METRIC_FETCH_CRON:0 0/1 0-7 * * ?} +experiment.metric.fetch.limit.per.interval=${EXPERIMENT_METRIC_FETCH_LIMIT_PER_INTERVAL:5} +population.graph.data.days.interval=${POPULATION_GRAPH_DATA_DAYS_INTERVAL:3} +query.schedule.and.end.time.diff.hours=${QUERY_SCHEDULE_AND_END_TIME_DIFF_HOURS:2} +query.day.interval.for.total.users=${QUERY_DAY_INTERVAL_FOR_TOTAL_USERS:30} \ No newline at end of file diff --git a/litmus-core/src/test/java/com/navi/medici/TestUtils.java b/litmus-core/src/test/java/com/navi/medici/TestUtils.java index 99c4296..cbcb040 100644 --- a/litmus-core/src/test/java/com/navi/medici/TestUtils.java +++ b/litmus-core/src/test/java/com/navi/medici/TestUtils.java @@ -10,6 +10,7 @@ import com.navi.medici.entity.ExperimentAuditTrailEntity; import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentInfoEntity; import com.navi.medici.entity.ExperimentMetricMappingEntity; +import com.navi.medici.entity.ExperimentMetricResultEntity; import com.navi.medici.entity.MetricEntity; import com.navi.medici.entity.SegmentEntity; import com.navi.medici.entity.TeamEntity; @@ -138,6 +139,18 @@ public class TestUtils { .build(); } + public static ExperimentMetricResultEntity getExperimentMetricResultEntity() { + return ExperimentMetricResultEntity.builder() + .timestamp(getLocalDateTime()) + .totalUsers(0) + .metric(getMetricEntity()) + .experiment(getExperimentEntity()) + .converted(0) + .notConverted(0) + .variantName(Constants.CONTROL) + .build(); + } + public static ExperimentInfoEntity getExperimentInfoEntity() { return ExperimentInfoEntity.builder() diff --git a/litmus-core/src/test/java/com/navi/medici/mapper/ExperimentMapperTest.java b/litmus-core/src/test/java/com/navi/medici/mapper/ExperimentMapperTest.java index e8d86c8..c47099c 100644 --- a/litmus-core/src/test/java/com/navi/medici/mapper/ExperimentMapperTest.java +++ b/litmus-core/src/test/java/com/navi/medici/mapper/ExperimentMapperTest.java @@ -18,6 +18,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Optional; import java.util.Set; @ExtendWith(MockitoExtension.class) @@ -36,12 +37,13 @@ class ExperimentMapperTest { .createdBy("sample-user") .experimentMetricMappings(Set.of(TestUtils.getExperimentMetricMappingEntity())) .build(); - DashboardExperimentResponse dashboardExperimentResponse = experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(experimentEntity); + DashboardExperimentResponse dashboardExperimentResponse = experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(experimentEntity, Optional.ofNullable(TestUtils.getMetricEntity()), TestUtils.getExperimentImpact()); Assertions.assertEquals("test metric", dashboardExperimentResponse.getPrimaryMetric()); Assertions.assertEquals(ExperimentImpact.builder() - .control(0.0) - .treatment(0.0) - .flag(null) + .control(1.0) + .treatment(2.0) + .variantName("test variant 1") + .flag(true) .build().toString(), dashboardExperimentResponse.getImpact().toString()); Assertions.assertEquals("uuid", dashboardExperimentResponse.getExperimentId()); Assertions.assertEquals("experiment-name", dashboardExperimentResponse.getExperimentName()); diff --git a/litmus-core/src/test/java/com/navi/medici/mapper/MetricMapperTest.java b/litmus-core/src/test/java/com/navi/medici/mapper/MetricMapperTest.java index 2bdbd5f..06b4ba0 100644 --- a/litmus-core/src/test/java/com/navi/medici/mapper/MetricMapperTest.java +++ b/litmus-core/src/test/java/com/navi/medici/mapper/MetricMapperTest.java @@ -1,5 +1,6 @@ package com.navi.medici.mapper; +import com.navi.medici.TestUtils; import com.navi.medici.entity.MetricEntity; import com.navi.medici.enums.MetricType; import com.navi.medici.response.MetricResponse; @@ -23,6 +24,7 @@ class MetricMapperTest { .createdBy("user") .athenaQuery("athena-query") .metricName("metric-name") + .updatedAt(TestUtils.getLocalDateTime()) .build(); MetricResponse response = metricMapper.mapMetricEntityToMetricResponse(metricEntity, 0); Assertions.assertEquals("uuid", response.getMetricId()); diff --git a/litmus-core/src/test/java/com/navi/medici/service/experiment/ExperimentServiceImplTest.java b/litmus-core/src/test/java/com/navi/medici/service/experiment/ExperimentServiceImplTest.java index b141de0..249e921 100644 --- a/litmus-core/src/test/java/com/navi/medici/service/experiment/ExperimentServiceImplTest.java +++ b/litmus-core/src/test/java/com/navi/medici/service/experiment/ExperimentServiceImplTest.java @@ -19,6 +19,7 @@ import com.navi.medici.query.experiment.IExperimentQuery; import com.navi.medici.query.experimentaudittrail.IExperimentAuditTrailQuery; import com.navi.medici.query.experimentinfo.IExperimentInfoQuery; import com.navi.medici.query.experimentmetricmapping.IExperimentMetricMappingQuery; +import com.navi.medici.query.experimentmetricresult.IExperimentMetricResultQuery; import com.navi.medici.query.metric.IMetricQuery; import com.navi.medici.query.team.ITeamQuery; import com.navi.medici.request.v1.AttachMetricToExperimentRequest; @@ -91,6 +92,8 @@ class ExperimentServiceImplTest { private JacksonUtils jacksonUtils; @Mock private IExperimentAuditTrailQuery experimentAuditTrailQuery; + @Mock + private IExperimentMetricResultQuery experimentMetricResultQuery; @Test @@ -101,7 +104,7 @@ class ExperimentServiceImplTest { MetricEntity metricEntity = TestUtils.getMetricEntity(); TeamEntity team = TestUtils.getTeamEntity(); Mockito.when(metricQuery.findByMetricName(any())).thenReturn(Optional.ofNullable(metricEntity)); - Mockito.when(experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(any())).thenReturn(response); + Mockito.when(experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(any(), any(), any())).thenReturn(response); Mockito.when(jacksonUtils.objectToString(any())).thenReturn(" "); Mockito.when(teamQuery.findByTeamName(any())).thenReturn(Optional.ofNullable(team)); Mockito.when(experimentQuery.findByExperimentId(any())).thenReturn(Optional.ofNullable(TestUtils.getExperimentEntity())); @@ -137,7 +140,8 @@ class ExperimentServiceImplTest { Page experimentEntities = new PageImpl<>(List.of(TestUtils.getExperimentEntity()), paging, 1); DashboardExperimentResponse dashboardExperimentResponse = TestUtils.getDashboardExperimentResponse(); Mockito.when(experimentQuery.findAll(any(), any())).thenReturn(experimentEntities); - Mockito.when(experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(any())).thenReturn(dashboardExperimentResponse); + Mockito.when(experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(any(), any(), any())).thenReturn(dashboardExperimentResponse); + Mockito.when(experimentMetricResultQuery.findByExperimentAndMetricOrderByIdAsc(any(), any())).thenReturn(List.of(TestUtils.getExperimentMetricResultEntity())); PaginatedSearchResponse response = experimentService.getExperiments(request); assertEquals(1, response.getTotalSize()); assertEquals("test experiment", response.getData().get(0).getExperimentName()); diff --git a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentEntity.java index 688bcec..9bd64f8 100644 --- a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentEntity.java +++ b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentEntity.java @@ -19,7 +19,6 @@ import javax.persistence.FetchType; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import javax.persistence.Table; -import java.io.Serializable; import java.time.LocalDateTime; import java.util.Set; @@ -31,7 +30,7 @@ import java.util.Set; @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) -public class ExperimentEntity extends BaseEntity implements Serializable { +public class ExperimentEntity extends BaseEntity { @Column(name = "experiment_id", unique = true) String experimentId; @@ -86,4 +85,8 @@ public class ExperimentEntity extends BaseEntity implements Serializable { @OneToMany(fetch = FetchType.LAZY, mappedBy = "experiment") @Cascade(CascadeType.ALL) Set experimentAuditTrails; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "experiment") + @Cascade(CascadeType.ALL) + Set tesseractIdStatuses; } diff --git a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricMappingEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricMappingEntity.java index 1456b63..129f8e5 100644 --- a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricMappingEntity.java +++ b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricMappingEntity.java @@ -1,6 +1,7 @@ package com.navi.medici.entity; import com.navi.medici.enums.ExperimentMetricType; +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -8,6 +9,8 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.FieldDefaults; import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; import org.hibernate.annotations.Cascade; import org.hibernate.annotations.CascadeType; @@ -17,6 +20,7 @@ import javax.persistence.Enumerated; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; +import java.util.Map; @Entity @Table(name = "experiment_metric_mapping") @@ -26,6 +30,7 @@ import javax.persistence.Table; @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) public class ExperimentMetricMappingEntity extends BaseEntity { @ManyToOne @@ -40,4 +45,9 @@ public class ExperimentMetricMappingEntity extends BaseEntity { @Enumerated(EnumType.STRING) ExperimentMetricType experimentMetricType; + + boolean isJobRun; + + @Type(type = "jsonb") + Map queryVariableMapping; } diff --git a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricResultEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricResultEntity.java index d035327..18dd9ec 100644 --- a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricResultEntity.java +++ b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricResultEntity.java @@ -1,34 +1,31 @@ package com.navi.medici.entity; -import com.navi.medici.dto.MetricResult; -import com.vladmihalcea.hibernate.type.json.JsonBinaryType; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; import lombok.experimental.FieldDefaults; import lombok.experimental.SuperBuilder; import org.hibernate.annotations.Cascade; import org.hibernate.annotations.CascadeType; -import org.hibernate.annotations.Type; -import org.hibernate.annotations.TypeDef; import javax.persistence.Entity; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; -import java.util.List; +import java.time.LocalDateTime; @Entity @Table(name = "experiment_metric_result") @Getter @Setter @SuperBuilder +@ToString @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) -@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) public class ExperimentMetricResultEntity extends BaseEntity { @ManyToOne @JoinColumn(name = "experiment_id") @@ -40,6 +37,11 @@ public class ExperimentMetricResultEntity extends BaseEntity { @Cascade(CascadeType.ALL) MetricEntity metric; - @Type(type = "jsonb") - List result; + String tesseractId; + + String variantName; + long converted; + long notConverted; + long totalUsers; + LocalDateTime timestamp; } diff --git a/litmus-db/src/main/java/com/navi/medici/entity/MetricEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/MetricEntity.java index 70ca1f3..901430d 100644 --- a/litmus-db/src/main/java/com/navi/medici/entity/MetricEntity.java +++ b/litmus-db/src/main/java/com/navi/medici/entity/MetricEntity.java @@ -1,6 +1,7 @@ package com.navi.medici.entity; import com.navi.medici.enums.MetricType; +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -8,12 +9,17 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.FieldDefaults; import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.Cascade; +import org.hibernate.annotations.CascadeType; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.OneToMany; import javax.persistence.Table; +import java.util.List; import java.util.Set; @Entity @@ -24,6 +30,7 @@ import java.util.Set; @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) public class MetricEntity extends BaseEntity { String metricId; @@ -44,4 +51,11 @@ public class MetricEntity extends BaseEntity { @OneToMany(mappedBy = "metric") Set experimentMetricResultEntities; + + @Type(type = "jsonb") + List queryVariables; + + @OneToMany(mappedBy = "metric") + @Cascade(CascadeType.ALL) + Set tesseractIdStatuses; } diff --git a/litmus-db/src/main/java/com/navi/medici/entity/TesseractIdStatusEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/TesseractIdStatusEntity.java new file mode 100644 index 0000000..ed0d209 --- /dev/null +++ b/litmus-db/src/main/java/com/navi/medici/entity/TesseractIdStatusEntity.java @@ -0,0 +1,41 @@ +package com.navi.medici.entity; + +import com.navi.medici.tesseract.request.TesseractIdStatus; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; + +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "tesseract_id_status") +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class TesseractIdStatusEntity extends BaseEntity { + + @ManyToOne + @JoinColumn(name = "experiment_id") + ExperimentEntity experiment; + + @ManyToOne + @JoinColumn(name = "metric_id") + MetricEntity metric; + + String tesseractId; + + @Enumerated(EnumType.STRING) + TesseractIdStatus status; +} diff --git a/litmus-db/src/main/java/com/navi/medici/query/experiment/ExperimentQueryImpl.java b/litmus-db/src/main/java/com/navi/medici/query/experiment/ExperimentQueryImpl.java index f0f4b1d..38af00a 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/experiment/ExperimentQueryImpl.java +++ b/litmus-db/src/main/java/com/navi/medici/query/experiment/ExperimentQueryImpl.java @@ -2,16 +2,16 @@ package com.navi.medici.query.experiment; import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.repository.ExperimentRepository; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Component; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + @Component @RequiredArgsConstructor public class ExperimentQueryImpl implements IExperimentQuery { @@ -51,13 +51,18 @@ public class ExperimentQueryImpl implements IExperimentQuery { } @Override - public Page findAll(Specification spec, Pageable pageable){ + public Page findAll(Specification spec, Pageable pageable) { return experimentRepository.findAll(spec, pageable); } + @Override + public Optional findById(long id) { + return experimentRepository.findById(id); + } + @Override - public List getDistinctCreatedBy(){ + public List getDistinctCreatedBy() { return experimentRepository.getDistinctCreatedBy(); } } diff --git a/litmus-db/src/main/java/com/navi/medici/query/experiment/IExperimentQuery.java b/litmus-db/src/main/java/com/navi/medici/query/experiment/IExperimentQuery.java index 03cffbf..ae5eb82 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/experiment/IExperimentQuery.java +++ b/litmus-db/src/main/java/com/navi/medici/query/experiment/IExperimentQuery.java @@ -24,4 +24,6 @@ public interface IExperimentQuery { ExperimentEntity save(ExperimentEntity experiment); Page findAll(Specification spec, Pageable pageable); + + Optional findById(long id); } diff --git a/litmus-db/src/main/java/com/navi/medici/query/experimentmetricmapping/ExperimentMetricMappingQueryImpl.java b/litmus-db/src/main/java/com/navi/medici/query/experimentmetricmapping/ExperimentMetricMappingQueryImpl.java index 31097e5..c3cc449 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/experimentmetricmapping/ExperimentMetricMappingQueryImpl.java +++ b/litmus-db/src/main/java/com/navi/medici/query/experimentmetricmapping/ExperimentMetricMappingQueryImpl.java @@ -1,11 +1,15 @@ package com.navi.medici.query.experimentmetricmapping; +import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentMetricMappingEntity; import com.navi.medici.entity.MetricEntity; import com.navi.medici.repository.ExperimentMetricMappingRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.util.List; +import java.util.Optional; + @Component @RequiredArgsConstructor public class ExperimentMetricMappingQueryImpl implements IExperimentMetricMappingQuery { @@ -21,4 +25,24 @@ public class ExperimentMetricMappingQueryImpl implements IExperimentMetricMappin public long countByMetric(MetricEntity metric) { return experimentMetricMappingRepository.countByMetric(metric); } + + @Override + public List findByIsJobRunLimitTo(boolean isJobRun, int experimentMetricFetchLimitPerInterval) { + return experimentMetricMappingRepository.findByIsJobRunLimitTo(isJobRun, experimentMetricFetchLimitPerInterval); + } + + @Override + public List findAll() { + return experimentMetricMappingRepository.findAll(); + } + + @Override + public List findByExperiment(ExperimentEntity experiment) { + return experimentMetricMappingRepository.findByExperiment(experiment); + } + + @Override + public Optional findByExperimentAndMetric(ExperimentEntity experiment, MetricEntity metric) { + return experimentMetricMappingRepository.findByExperimentAndMetric(experiment, metric); + } } diff --git a/litmus-db/src/main/java/com/navi/medici/query/experimentmetricmapping/IExperimentMetricMappingQuery.java b/litmus-db/src/main/java/com/navi/medici/query/experimentmetricmapping/IExperimentMetricMappingQuery.java index 8ea464a..01ad57a 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/experimentmetricmapping/IExperimentMetricMappingQuery.java +++ b/litmus-db/src/main/java/com/navi/medici/query/experimentmetricmapping/IExperimentMetricMappingQuery.java @@ -1,10 +1,22 @@ package com.navi.medici.query.experimentmetricmapping; +import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentMetricMappingEntity; import com.navi.medici.entity.MetricEntity; +import java.util.List; +import java.util.Optional; + public interface IExperimentMetricMappingQuery { ExperimentMetricMappingEntity save(ExperimentMetricMappingEntity experimentMetricMapping); long countByMetric(MetricEntity metric); + + List findByIsJobRunLimitTo(boolean isJobRun, int experimentMetricFetchLimitPerInterval); + + List findAll(); + + List findByExperiment(ExperimentEntity experiment); + + Optional findByExperimentAndMetric(ExperimentEntity experiment, MetricEntity metric); } diff --git a/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/ExperimentMetricResultQueryImpl.java b/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/ExperimentMetricResultQueryImpl.java index 17e6333..d631103 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/ExperimentMetricResultQueryImpl.java +++ b/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/ExperimentMetricResultQueryImpl.java @@ -1,10 +1,16 @@ package com.navi.medici.query.experimentmetricresult; +import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentMetricResultEntity; +import com.navi.medici.entity.MetricEntity; import com.navi.medici.repository.ExperimentMetricResultRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + @Component @RequiredArgsConstructor public class ExperimentMetricResultQueryImpl implements IExperimentMetricResultQuery { @@ -16,4 +22,24 @@ public class ExperimentMetricResultQueryImpl implements IExperimentMetricResultQ return experimentMetricResultRepository.save(experimentMetricResult); } + @Override + public Optional findByTesseractIdAndVariantName(String tesseractId, String variantName) { + return experimentMetricResultRepository.findByTesseractIdAndVariantName(tesseractId, variantName); + } + + @Override + public List findByExperimentAndMetricOrderByIdAsc(ExperimentEntity experiment, MetricEntity metric) { + return experimentMetricResultRepository.findByExperimentAndMetricOrderByIdAsc(experiment, metric); + } + + @Override + public List findByExperiment(ExperimentEntity experiment) { + return experimentMetricResultRepository.findByExperiment(experiment); + } + + @Override + public List findByExperimentUpdatedAfter(ExperimentEntity experiment, LocalDateTime updatedAfter) { + return experimentMetricResultRepository.findByExperimentAndUpdatedAtAfter(experiment, updatedAfter); + } + } diff --git a/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/IExperimentMetricResultQuery.java b/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/IExperimentMetricResultQuery.java index a29a993..af5f0c3 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/IExperimentMetricResultQuery.java +++ b/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/IExperimentMetricResultQuery.java @@ -1,7 +1,21 @@ package com.navi.medici.query.experimentmetricresult; +import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentMetricResultEntity; +import com.navi.medici.entity.MetricEntity; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; public interface IExperimentMetricResultQuery { ExperimentMetricResultEntity save(ExperimentMetricResultEntity experimentMetricResult); + + Optional findByTesseractIdAndVariantName(String tesseractId, String variantName); + + List findByExperimentAndMetricOrderByIdAsc(ExperimentEntity experiment, MetricEntity metric); + + List findByExperiment(ExperimentEntity experiment); + + List findByExperimentUpdatedAfter(ExperimentEntity experiment, LocalDateTime updatedAfter); } diff --git a/litmus-db/src/main/java/com/navi/medici/query/metric/IMetricQuery.java b/litmus-db/src/main/java/com/navi/medici/query/metric/IMetricQuery.java index ea0d917..dbda3be 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/metric/IMetricQuery.java +++ b/litmus-db/src/main/java/com/navi/medici/query/metric/IMetricQuery.java @@ -16,4 +16,6 @@ public interface IMetricQuery { MetricEntity save(MetricEntity metric); List getAllMetricNames(); + + Optional findById(long id); } diff --git a/litmus-db/src/main/java/com/navi/medici/query/metric/MetricQueryImpl.java b/litmus-db/src/main/java/com/navi/medici/query/metric/MetricQueryImpl.java index e22f55f..b010cc7 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/metric/MetricQueryImpl.java +++ b/litmus-db/src/main/java/com/navi/medici/query/metric/MetricQueryImpl.java @@ -35,4 +35,9 @@ public class MetricQueryImpl implements IMetricQuery { public List getAllMetricNames() { return metricRepository.getAllMetricNames(); } + + @Override + public Optional findById(long id) { + return metricRepository.findById(id); + } } diff --git a/litmus-db/src/main/java/com/navi/medici/query/tesseractidstatus/ITesseractIdStatusQuery.java b/litmus-db/src/main/java/com/navi/medici/query/tesseractidstatus/ITesseractIdStatusQuery.java new file mode 100644 index 0000000..30c498a --- /dev/null +++ b/litmus-db/src/main/java/com/navi/medici/query/tesseractidstatus/ITesseractIdStatusQuery.java @@ -0,0 +1,11 @@ +package com.navi.medici.query.tesseractidstatus; + +import com.navi.medici.entity.TesseractIdStatusEntity; + +import java.util.Optional; + +public interface ITesseractIdStatusQuery { + void save(TesseractIdStatusEntity tesseractIdStatus); + + Optional findByTesseractId(String tesseractId); +} diff --git a/litmus-db/src/main/java/com/navi/medici/query/tesseractidstatus/TesseractIdStatusQueryImpl.java b/litmus-db/src/main/java/com/navi/medici/query/tesseractidstatus/TesseractIdStatusQueryImpl.java new file mode 100644 index 0000000..141feb2 --- /dev/null +++ b/litmus-db/src/main/java/com/navi/medici/query/tesseractidstatus/TesseractIdStatusQueryImpl.java @@ -0,0 +1,27 @@ +package com.navi.medici.query.tesseractidstatus; + +import com.navi.medici.entity.TesseractIdStatusEntity; +import com.navi.medici.repository.TesseractIdStatusRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +@Log4j2 +@RequiredArgsConstructor +public class TesseractIdStatusQueryImpl implements ITesseractIdStatusQuery { + + private final TesseractIdStatusRepository tesseractIdStatusRepository; + + @Override + public void save(TesseractIdStatusEntity tesseractIdStatus) { + tesseractIdStatusRepository.save(tesseractIdStatus); + } + + @Override + public Optional findByTesseractId(String tesseractId) { + return tesseractIdStatusRepository.findByTesseractId(tesseractId); + } +} diff --git a/litmus-db/src/main/java/com/navi/medici/repository/ExperimentMetricMappingRepository.java b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentMetricMappingRepository.java index afd9195..05f868e 100644 --- a/litmus-db/src/main/java/com/navi/medici/repository/ExperimentMetricMappingRepository.java +++ b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentMetricMappingRepository.java @@ -1,11 +1,26 @@ package com.navi.medici.repository; +import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentMetricMappingEntity; import com.navi.medici.entity.MetricEntity; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public interface ExperimentMetricMappingRepository extends CrudRepository { long countByMetric(MetricEntity metric); + + @Query(value = "select * from experiment_metric_mapping emm where emm.is_job_run=:isJobRun and emm.experiment_id in (select ei.experiment_id from experiment_info ei where ei.experiment_status = 'RUNNING') order by updated_at asc limit :experimentMetricFetchLimitPerInterval", nativeQuery = true) + List findByIsJobRunLimitTo(boolean isJobRun, int experimentMetricFetchLimitPerInterval); + + @Query(value = "select * from experiment_metric_mapping", nativeQuery = true) + List findAll(); + + List findByExperiment(ExperimentEntity experiment); + + Optional findByExperimentAndMetric(ExperimentEntity experiment, MetricEntity metric); } diff --git a/litmus-db/src/main/java/com/navi/medici/repository/ExperimentMetricResultRepository.java b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentMetricResultRepository.java index 9cc8879..7db4dba 100644 --- a/litmus-db/src/main/java/com/navi/medici/repository/ExperimentMetricResultRepository.java +++ b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentMetricResultRepository.java @@ -1,9 +1,22 @@ package com.navi.medici.repository; +import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentMetricResultEntity; +import com.navi.medici.entity.MetricEntity; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + @Repository public interface ExperimentMetricResultRepository extends CrudRepository { + Optional findByTesseractIdAndVariantName(String tesseractId, String variantName); + + List findByExperimentAndMetricOrderByIdAsc(ExperimentEntity experiment, MetricEntity metric); + + List findByExperiment(ExperimentEntity experiment); + + List findByExperimentAndUpdatedAtAfter(ExperimentEntity experiment, LocalDateTime updatedAfter); } diff --git a/litmus-db/src/main/java/com/navi/medici/repository/TesseractIdStatusRepository.java b/litmus-db/src/main/java/com/navi/medici/repository/TesseractIdStatusRepository.java new file mode 100644 index 0000000..8e2ee44 --- /dev/null +++ b/litmus-db/src/main/java/com/navi/medici/repository/TesseractIdStatusRepository.java @@ -0,0 +1,12 @@ +package com.navi.medici.repository; + +import com.navi.medici.entity.TesseractIdStatusEntity; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface TesseractIdStatusRepository extends CrudRepository { + Optional findByTesseractId(String tesseractId); +} diff --git a/litmus-liquibase/src/main/resources/db/changelog/202304031733-add-tesseract-id-to-experiment-metric-result.sql b/litmus-liquibase/src/main/resources/db/changelog/202304031733-add-tesseract-id-to-experiment-metric-result.sql new file mode 100644 index 0000000..855ba36 --- /dev/null +++ b/litmus-liquibase/src/main/resources/db/changelog/202304031733-add-tesseract-id-to-experiment-metric-result.sql @@ -0,0 +1,7 @@ +--liquibase formatted sql + +--changeset author:akshatsoni id:202304031733 +ALTER TABLE experiment_metric_result + ADD COLUMN tesseract_id varchar(36); + +CREATE INDEX idx_tesseract_id ON experiment_metric_result (tesseract_id); \ No newline at end of file diff --git a/litmus-liquibase/src/main/resources/db/changelog/202304041852-add-is-job-run-in-experiment-metric-mapping.sql b/litmus-liquibase/src/main/resources/db/changelog/202304041852-add-is-job-run-in-experiment-metric-mapping.sql new file mode 100644 index 0000000..6cd3161 --- /dev/null +++ b/litmus-liquibase/src/main/resources/db/changelog/202304041852-add-is-job-run-in-experiment-metric-mapping.sql @@ -0,0 +1,5 @@ +--liquibase formatted sql + +--changeset author:akshatsoni id:202304041852 +ALTER TABLE experiment_metric_mapping + ADD COLUMN is_job_run boolean DEFAULT false; \ No newline at end of file diff --git a/litmus-liquibase/src/main/resources/db/changelog/202304042036-add-variable-mapping.sql b/litmus-liquibase/src/main/resources/db/changelog/202304042036-add-variable-mapping.sql new file mode 100644 index 0000000..6763513 --- /dev/null +++ b/litmus-liquibase/src/main/resources/db/changelog/202304042036-add-variable-mapping.sql @@ -0,0 +1,8 @@ +--liquibase formatted sql + +--changeset author:akshatsoni id:202304042036 +ALTER TABLE experiment_metric_mapping + ADD COLUMN query_variable_mapping jsonb; + +ALTER TABLE metrics + ADD COLUMN query_variables jsonb; \ No newline at end of file diff --git a/litmus-liquibase/src/main/resources/db/changelog/202304061847-alter-experiment-metric-result-table.sql b/litmus-liquibase/src/main/resources/db/changelog/202304061847-alter-experiment-metric-result-table.sql new file mode 100644 index 0000000..e269801 --- /dev/null +++ b/litmus-liquibase/src/main/resources/db/changelog/202304061847-alter-experiment-metric-result-table.sql @@ -0,0 +1,10 @@ +--liquibase formatted sql + +--changeset author:akshatsoni id:202304061847 +ALTER TABLE experiment_metric_result + DROP COLUMN result, + ADD COLUMN variant_name varchar(255), + ADD COLUMN converted bigint, + ADD COLUMN not_converted bigint, + ADD COLUMN total_users bigint, + ADD COLUMN timestamp timestamp; \ No newline at end of file diff --git a/litmus-liquibase/src/main/resources/db/changelog/202304091625-create-tesseract-id-status-table.sql b/litmus-liquibase/src/main/resources/db/changelog/202304091625-create-tesseract-id-status-table.sql new file mode 100644 index 0000000..279efcf --- /dev/null +++ b/litmus-liquibase/src/main/resources/db/changelog/202304091625-create-tesseract-id-status-table.sql @@ -0,0 +1,16 @@ +--liquibase formatted sql + +--changeset author:akshatsoni id:202304091625 +CREATE TABLE tesseract_id_status +( + id SERIAL PRIMARY KEY, + experiment_id int references experiments (id), + metric_id int references metrics (id), + tesseract_id varchar(36), + status varchar(50), + created_at timestamp, + updated_at timestamp, + version bigint +); + +CREATE INDEX idx_tesseract_id_status ON tesseract_id_status (tesseract_id); \ No newline at end of file diff --git a/litmus-liquibase/src/main/resources/db/changelog/202304132304-create-shedlock-table.sql b/litmus-liquibase/src/main/resources/db/changelog/202304132304-create-shedlock-table.sql new file mode 100644 index 0000000..9b5a070 --- /dev/null +++ b/litmus-liquibase/src/main/resources/db/changelog/202304132304-create-shedlock-table.sql @@ -0,0 +1,11 @@ +--liquibase formatted sql + +--changeset author:akshatsoni id:202304132304 +CREATE TABLE shedlock +( + name VARCHAR(64) NOT NULL, + lock_until TIMESTAMP NOT NULL, + locked_at TIMESTAMP NOT NULL, + locked_by VARCHAR(255) NOT NULL, + PRIMARY KEY (name) +); \ No newline at end of file diff --git a/litmus-model/pom.xml b/litmus-model/pom.xml index 8f143b1..b979c5c 100644 --- a/litmus-model/pom.xml +++ b/litmus-model/pom.xml @@ -69,6 +69,10 @@ javax.persistence javax.persistence-api + + javax.persistence + javax.persistence-api + diff --git a/litmus-model/src/main/java/com/navi/medici/dto/DoughnutSectionDTO.java b/litmus-model/src/main/java/com/navi/medici/dto/DoughnutSectionDTO.java new file mode 100644 index 0000000..9773e76 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/dto/DoughnutSectionDTO.java @@ -0,0 +1,18 @@ +package com.navi.medici.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class DoughnutSectionDTO { + String key; + long value; +} diff --git a/litmus-model/src/main/java/com/navi/medici/dto/ExperimentDataDTO.java b/litmus-model/src/main/java/com/navi/medici/dto/ExperimentDataDTO.java new file mode 100644 index 0000000..c660024 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/dto/ExperimentDataDTO.java @@ -0,0 +1,22 @@ +package com.navi.medici.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +import java.util.List; +import java.util.Map; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ExperimentDataDTO { + List> metricTable; + List populationDoughnut; + List> populationGraph; +} diff --git a/litmus-model/src/main/java/com/navi/medici/dto/GraphDTO.java b/litmus-model/src/main/java/com/navi/medici/dto/GraphDTO.java new file mode 100644 index 0000000..35a9635 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/dto/GraphDTO.java @@ -0,0 +1,22 @@ +package com.navi.medici.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class GraphDTO { + String name; + List values; + List timestamps; +} diff --git a/litmus-model/src/main/java/com/navi/medici/dto/MetricResult.java b/litmus-model/src/main/java/com/navi/medici/dto/MetricResult.java index dfcfc5c..6655c4c 100644 --- a/litmus-model/src/main/java/com/navi/medici/dto/MetricResult.java +++ b/litmus-model/src/main/java/com/navi/medici/dto/MetricResult.java @@ -4,10 +4,16 @@ import lombok.AccessLevel; import lombok.Data; import lombok.experimental.FieldDefaults; +import java.time.LocalDateTime; + @Data @FieldDefaults(level = AccessLevel.PRIVATE) public class MetricResult { - String name; + long experimentId; + long metricId; + String variantName; long converted; long notConverted; + long totalUsers; + LocalDateTime timestasmp; } diff --git a/litmus-model/src/main/java/com/navi/medici/dto/TableDTO.java b/litmus-model/src/main/java/com/navi/medici/dto/TableDTO.java new file mode 100644 index 0000000..5804cfb --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/dto/TableDTO.java @@ -0,0 +1,20 @@ +package com.navi.medici.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +import java.util.List; +import java.util.Map; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class TableDTO { + List> rows; +} diff --git a/litmus-model/src/main/java/com/navi/medici/response/DashboardExperimentResponse.java b/litmus-model/src/main/java/com/navi/medici/response/DashboardExperimentResponse.java index 53bedb3..7ff8ab2 100644 --- a/litmus-model/src/main/java/com/navi/medici/response/DashboardExperimentResponse.java +++ b/litmus-model/src/main/java/com/navi/medici/response/DashboardExperimentResponse.java @@ -25,6 +25,7 @@ public class DashboardExperimentResponse { String createdBy; String primaryMetric; ExperimentImpact impact; + double progressPercent; long testUsers; ExperimentStatus experimentStatus; } diff --git a/litmus-model/src/main/java/com/navi/medici/response/ExperimentResponse.java b/litmus-model/src/main/java/com/navi/medici/response/ExperimentResponse.java index 651d57b..6c73cc7 100644 --- a/litmus-model/src/main/java/com/navi/medici/response/ExperimentResponse.java +++ b/litmus-model/src/main/java/com/navi/medici/response/ExperimentResponse.java @@ -29,6 +29,7 @@ public class ExperimentResponse { ExperimentType type; ExperimentInfoDTO experimentInfo; String vertical; + String result; List metrics; LocalDateTime createdAt; LocalDateTime updatedAt; diff --git a/litmus-model/src/main/java/com/navi/medici/tesseract/request/SyncType.java b/litmus-model/src/main/java/com/navi/medici/tesseract/request/SyncType.java new file mode 100644 index 0000000..2131395 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/tesseract/request/SyncType.java @@ -0,0 +1,5 @@ +package com.navi.medici.tesseract.request; + +public enum SyncType { + ADHOC, RECURRING +} \ No newline at end of file diff --git a/litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractCSVRegisterRequest.java b/litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractCSVRegisterRequest.java new file mode 100644 index 0000000..50b1a53 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractCSVRegisterRequest.java @@ -0,0 +1,56 @@ +package com.navi.medici.tesseract.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TesseractCSVRegisterRequest { + + Source sourceConfig; + String syncType; + String transitionCallback; + String team; + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Source { + String sourceType; + Attributes attributes; + } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Attributes { + List columns; + String separator; + String database; + String tableName; + + } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Column { + String name; + String type; + } +} + diff --git a/litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractIdStatus.java b/litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractIdStatus.java new file mode 100644 index 0000000..f8b3aa0 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractIdStatus.java @@ -0,0 +1,10 @@ +package com.navi.medici.tesseract.request; + +public enum TesseractIdStatus { + PENDING, + SOURCE_RUNNING, + SOURCE_COMPLETED, + DESTINATION_RUNNING, + DESTINATION_FAILED, + COMPLETED +} diff --git a/litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractQueryRegisterRequest.java b/litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractQueryRegisterRequest.java new file mode 100644 index 0000000..b7bfa4a --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractQueryRegisterRequest.java @@ -0,0 +1,64 @@ +package com.navi.medici.tesseract.request; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonIgnoreProperties +public class TesseractQueryRegisterRequest { + Source sourceConfig; + Destination destinationConfig; + SyncType syncType; + String cronExpression; + String transitionCallback; + String team; + + + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class Source { + String sourceType; + Attributes attributes; + } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class Destination { + String destinationType; + Attributes attributes; + } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class Attributes { + String queryText; + String s3Path; + String kafkaTopic; + String clusterName; + } +} diff --git a/litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractQueryUpdateRequest.java b/litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractQueryUpdateRequest.java new file mode 100644 index 0000000..d7ffe19 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/tesseract/request/TesseractQueryUpdateRequest.java @@ -0,0 +1,21 @@ +package com.navi.medici.tesseract.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TesseractQueryUpdateRequest { + String queryString; + String kafkaTopic; + SyncType syncType; + String cron; + String transitionCallback; +} + diff --git a/litmus-model/src/main/java/com/navi/medici/tesseract/response/QueryStatus.java b/litmus-model/src/main/java/com/navi/medici/tesseract/response/QueryStatus.java new file mode 100644 index 0000000..dbca6cc --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/tesseract/response/QueryStatus.java @@ -0,0 +1,10 @@ +package com.navi.medici.tesseract.response; + +public enum QueryStatus { + Pending, + Materialising, + Materialised, + Ingesting, + Ingested, + Completed +} \ No newline at end of file diff --git a/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractIdStatusResponse.java b/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractIdStatusResponse.java new file mode 100644 index 0000000..4e12153 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractIdStatusResponse.java @@ -0,0 +1,17 @@ +package com.navi.medici.tesseract.response; + +import com.navi.medici.tesseract.request.TesseractIdStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class TesseractIdStatusResponse { + private String tesseractId; + private Long jobRunId; + private TesseractIdStatus status; +} diff --git a/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryDeleteResponse.java b/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryDeleteResponse.java new file mode 100644 index 0000000..3d70d23 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryDeleteResponse.java @@ -0,0 +1,16 @@ +package com.navi.medici.tesseract.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TesseractQueryDeleteResponse { + boolean success; +} diff --git a/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryRegisterResponse.java b/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryRegisterResponse.java new file mode 100644 index 0000000..e01721b --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryRegisterResponse.java @@ -0,0 +1,16 @@ +package com.navi.medici.tesseract.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TesseractQueryRegisterResponse { + String tesseractId; +} diff --git a/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryStatsResponse.java b/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryStatsResponse.java new file mode 100644 index 0000000..903438c --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryStatsResponse.java @@ -0,0 +1,44 @@ +package com.navi.medici.tesseract.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +public class TesseractQueryStatsResponse { + String tesseractId; + SourceConfig sourceConfig; + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class SourceConfig { + Attributes attributes; + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class Attributes { + String tableName; + } + } +} + diff --git a/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryStatusResponse.java b/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryStatusResponse.java new file mode 100644 index 0000000..5085138 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryStatusResponse.java @@ -0,0 +1,5 @@ +package com.navi.medici.tesseract.response; + +public class TesseractQueryStatusResponse { + QueryStatus queryStatus; +} \ No newline at end of file diff --git a/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryUpdateResponse.java b/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryUpdateResponse.java new file mode 100644 index 0000000..e981038 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/tesseract/response/TesseractQueryUpdateResponse.java @@ -0,0 +1,16 @@ +package com.navi.medici.tesseract.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TesseractQueryUpdateResponse { + boolean success; +} diff --git a/litmus-util/pom.xml b/litmus-util/pom.xml index 7bda28e..9622eab 100644 --- a/litmus-util/pom.xml +++ b/litmus-util/pom.xml @@ -1,28 +1,35 @@ - - 4.0.0 - - litmus - com.navi.medici + + 4.0.0 + + litmus + com.navi.medici + 2.0.8-RELEASE + + + litmus-util 2.0.8-RELEASE - - litmus-util - 2.0.8-RELEASE + litmus-util - litmus-util + + + io.micrometer + micrometer-registry-prometheus + 1.9.0 + - - - io.micrometer - micrometer-registry-prometheus - 1.9.0 - - - org.springframework.boot - spring-boot-starter-actuator - + + org.freemarker + freemarker + + + + org.springframework.boot + spring-boot-starter-actuator + org.springframework.boot @@ -34,7 +41,13 @@ 3.6.1 compile + + org.apache.commons + commons-math3 + 3.6.1 + compile + - + diff --git a/litmus-util/src/main/java/com/navi/medici/exceptions/DuplicateMetricMappingToExperimentException.java b/litmus-util/src/main/java/com/navi/medici/exceptions/DuplicateMetricMappingToExperimentException.java new file mode 100644 index 0000000..eac86c1 --- /dev/null +++ b/litmus-util/src/main/java/com/navi/medici/exceptions/DuplicateMetricMappingToExperimentException.java @@ -0,0 +1,7 @@ +package com.navi.medici.exceptions; + +public class DuplicateMetricMappingToExperimentException extends RuntimeException { + public DuplicateMetricMappingToExperimentException(String message) { + super(message); + } +} diff --git a/litmus-util/src/main/java/com/navi/medici/exceptions/NoExperimentsFoundForVerticalException.java b/litmus-util/src/main/java/com/navi/medici/exceptions/NoExperimentsFoundForVerticalException.java index 24dec61..6e5c5f1 100644 --- a/litmus-util/src/main/java/com/navi/medici/exceptions/NoExperimentsFoundForVerticalException.java +++ b/litmus-util/src/main/java/com/navi/medici/exceptions/NoExperimentsFoundForVerticalException.java @@ -8,8 +8,4 @@ public class NoExperimentsFoundForVerticalException extends RuntimeException { super(message); } - public NoExperimentsFoundForVerticalException(String message, Throwable e) { - super(message, e); - log.error(message, e); - } } diff --git a/litmus-util/src/main/java/com/navi/medici/exceptions/StatisticalException.java b/litmus-util/src/main/java/com/navi/medici/exceptions/StatisticalException.java new file mode 100644 index 0000000..ae088ca --- /dev/null +++ b/litmus-util/src/main/java/com/navi/medici/exceptions/StatisticalException.java @@ -0,0 +1,7 @@ +package com.navi.medici.exceptions; + +public class StatisticalException extends RuntimeException { + public StatisticalException(String message) { + super(message); + } +} diff --git a/litmus-util/src/main/java/com/navi/medici/freemarker/FreeMarkerUtil.java b/litmus-util/src/main/java/com/navi/medici/freemarker/FreeMarkerUtil.java new file mode 100644 index 0000000..11d15ad --- /dev/null +++ b/litmus-util/src/main/java/com/navi/medici/freemarker/FreeMarkerUtil.java @@ -0,0 +1,59 @@ +package com.navi.medici.freemarker; + +import com.navi.medici.metrics.MetricsUtils; +import freemarker.core.InvalidReferenceException; +import freemarker.template.Configuration; +import freemarker.template.TemplateException; +import io.micrometer.core.instrument.MeterRegistry; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FreeMarkerUtil { + private static MeterRegistry meterRegistry; + + public static List getVariablesFromFreeMarkerMessage(String name, String title, String message) throws IOException { + Configuration freemarkerConfiguration = new Configuration(Configuration.VERSION_2_3_29); + freemarker.template.Template template = + new freemarker.template.Template(name, title, + new StringReader(message), freemarkerConfiguration + ); + StringWriter stringWriter = new StringWriter(); + Map dataModel = new HashMap<>(); + boolean exceptionCaught; + + do { + exceptionCaught = false; + try { + template.process(dataModel, stringWriter); + } catch (InvalidReferenceException e) { + exceptionCaught = true; + dataModel.put(e.getBlamedExpressionString(), ""); + } catch (IOException | TemplateException e) { + MetricsUtils.tesseractMetrics("freemarker_template_load_failure") + .register(meterRegistry) + .increment(); + throw new IllegalStateException("Failed to Load Template: " + template, e); + } + } while (exceptionCaught); + + return dataModel.keySet().stream().toList(); + } + + public static String processFreeMarkerMessage(String name, String title, String message, Map variableMap) throws IOException, TemplateException { + Configuration freemarkerConfiguration = new Configuration(Configuration.VERSION_2_3_29); + freemarker.template.Template freemarkerTemplate = + new freemarker.template.Template(name, title, + new StringReader(message), freemarkerConfiguration + ); + + final StringWriter processedTemplate = new StringWriter(message.length()); + freemarkerTemplate.process(variableMap, processedTemplate); + + return processedTemplate.toString(); + } +} diff --git a/litmus-util/src/main/java/com/navi/medici/metrics/MetricsUtils.java b/litmus-util/src/main/java/com/navi/medici/metrics/MetricsUtils.java new file mode 100644 index 0000000..5453122 --- /dev/null +++ b/litmus-util/src/main/java/com/navi/medici/metrics/MetricsUtils.java @@ -0,0 +1,13 @@ +package com.navi.medici.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; + +public class MetricsUtils { + + private MeterRegistry meterRegistry; + + public static Counter.Builder tesseractMetrics(String name) { + return Counter.builder(name); + } +} diff --git a/litmus-util/src/main/java/com/navi/medici/stats/ChiSquaredTest.java b/litmus-util/src/main/java/com/navi/medici/stats/ChiSquaredTest.java new file mode 100644 index 0000000..ec4b6fc --- /dev/null +++ b/litmus-util/src/main/java/com/navi/medici/stats/ChiSquaredTest.java @@ -0,0 +1,36 @@ +package com.navi.medici.stats; + +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.math3.stat.inference.TestUtils.chiSquareTest; + +public class ChiSquaredTest { + public static boolean chiSquaredTest(List converted, List notConverted, double alpha) { + long totalConverted = converted.stream().reduce(0L, Long::sum); + long totalNotConverted = notConverted.stream().reduce(0L, Long::sum); + long totalVisits = totalConverted + totalNotConverted; + List expectedConverted = new ArrayList<>(); + List expectedNotConverted = new ArrayList<>(); + List observed = new ArrayList<>(); + List expected = new ArrayList<>(); + converted.forEach(value -> { + if (value != 0) { + observed.add(value); + expected.add((double) (value * totalConverted) / totalVisits); + } + }); + notConverted.forEach(value -> { + if (value != 0) { + observed.add(value); + expected.add((double) (value * totalNotConverted) / totalVisits); + } + }); + if (observed.size() == 0) { + return false; + } + double[] exp = expected.stream().mapToDouble(Double::doubleValue).toArray(); + long[] obs = observed.stream().mapToLong(Long::longValue).toArray(); + return chiSquareTest(exp, obs, alpha); + } +}