diff --git a/litmus-client/pom.xml b/litmus-client/pom.xml index e936919..dd4be6d 100644 --- a/litmus-client/pom.xml +++ b/litmus-client/pom.xml @@ -75,6 +75,13 @@ jackson-datatype-jsr310 2.13.0 + + + io.micrometer + micrometer-registry-prometheus + 1.8.4 + + diff --git a/litmus-client/src/main/java/com/navi/medici/client/ClickStreamClient.java b/litmus-client/src/main/java/com/navi/medici/client/ClickStreamClient.java index ef596d5..ed14b31 100644 --- a/litmus-client/src/main/java/com/navi/medici/client/ClickStreamClient.java +++ b/litmus-client/src/main/java/com/navi/medici/client/ClickStreamClient.java @@ -3,7 +3,9 @@ package com.navi.medici.client; import com.navi.medici.clickstream.ClickStreamPayload; import com.navi.medici.config.LitmusConfig; +import com.navi.medici.response.LitmusExperimentResponse; import com.navi.medici.util.JacksonUtils; +import io.micrometer.core.instrument.Counter; import lombok.extern.log4j.Log4j2; import okhttp3.MediaType; import okhttp3.OkHttpClient; @@ -24,7 +26,7 @@ public class ClickStreamClient { this.client = new OkHttpClient(); } - public void publish(ClickStreamPayload payload) { + public void publish(String experimentName, ClickStreamPayload payload) { String requestBody = JacksonUtils.objectToString(payload); Request request = new Request.Builder() .url(this.litmusConfig.getClickStreamAPI()) @@ -32,6 +34,13 @@ public class ClickStreamClient { .build(); try(Response response = client.newCall(request).execute()) { + Counter.builder("litmus_client_click_stream_event_ingestion") + .tag("vertical", litmusConfig.getVertical()) + .tag("status", String.valueOf(response.code())) + .tag("experiment_name", experimentName) + .tag("app_name", litmusConfig.getAppName()) + .register(this.litmusConfig.getMeterRegistry()) + .increment(); if (response.code() < 300) { var responseBody = response.body(); assert responseBody != null; @@ -41,6 +50,14 @@ public class ClickStreamClient { log.error("clickstream ingestion received non-2xx. status: {}", response.code()); } } catch (Exception e) { + Counter.builder("litmus_client_click_stream_event_ingestion") + .tag("vertical", litmusConfig.getVertical()) + .tag("experiment_name", experimentName) + .tag("app_name", litmusConfig.getAppName()) + .tag("exception", e.getMessage()) + .register(this.litmusConfig.getMeterRegistry()) + .increment(); + log.error("clickstream event ingestion failed. ", e); } } diff --git a/litmus-client/src/main/java/com/navi/medici/client/ExperimentFetcher.java b/litmus-client/src/main/java/com/navi/medici/client/ExperimentFetcher.java index a6ebc91..f8dc6f9 100644 --- a/litmus-client/src/main/java/com/navi/medici/client/ExperimentFetcher.java +++ b/litmus-client/src/main/java/com/navi/medici/client/ExperimentFetcher.java @@ -5,5 +5,5 @@ import com.navi.medici.response.LitmusExperimentResponse; public interface ExperimentFetcher { - LitmusExperimentResponse fetchExperiments() throws LitmusException; + LitmusExperimentResponse fetchExperiments(String vertical, Long pollingTime) throws LitmusException; } diff --git a/litmus-client/src/main/java/com/navi/medici/client/HttpExperimentFetcher.java b/litmus-client/src/main/java/com/navi/medici/client/HttpExperimentFetcher.java index a2b6b8d..ff8f17b 100644 --- a/litmus-client/src/main/java/com/navi/medici/client/HttpExperimentFetcher.java +++ b/litmus-client/src/main/java/com/navi/medici/client/HttpExperimentFetcher.java @@ -6,6 +6,7 @@ import com.navi.medici.response.LitmusExperimentCollection; import com.navi.medici.response.LitmusExperimentResponse; import com.navi.medici.response.LitmusResponse; import com.navi.medici.util.JacksonUtils; +import io.micrometer.core.instrument.Counter; import java.net.URL; import lombok.extern.log4j.Log4j2; import okhttp3.OkHttpClient; @@ -14,10 +15,10 @@ import okhttp3.Response; @Log4j2 public class HttpExperimentFetcher implements ExperimentFetcher { - private final OkHttpClient client; private final URL experimentUrl; private final URL segmentIdUrl; + private final LitmusConfig litmusConfig; public HttpExperimentFetcher(LitmusConfig litmusConfig) { this.client = new OkHttpClient(); @@ -25,29 +26,64 @@ public class HttpExperimentFetcher implements ExperimentFetcher { litmusConfig.getLitmusURLs() .getLitmusExperimentsURL(litmusConfig.getProjectName(), litmusConfig.getNamePrefix()); this.segmentIdUrl = litmusConfig.getLitmusURLs().getSegmentIdURL(); + this.litmusConfig = litmusConfig; } @Override - public LitmusExperimentResponse fetchExperiments() throws LitmusException { + public LitmusExperimentResponse fetchExperiments(String vertical, Long pollingTime) throws LitmusException { Request request = new Request.Builder() - .url(this.experimentUrl) + .url(String.format("%s?vertical=%s&polling_time=%s", this.experimentUrl, vertical, pollingTime)) .build(); try (Response response = client.newCall(request).execute()) { + Counter.builder("litmus_client_fetch_experiment_polling") + .tag("vertical", vertical) + .tag("app_name", litmusConfig.getAppName()) + .register(this.litmusConfig.getMeterRegistry()) + .increment(); + if (response.code() < 300) { var responseBody = response.body(); assert responseBody != null; LitmusExperimentCollection experiments = JacksonUtils.stringToObject(responseBody.string(), LitmusExperimentCollection.class); + Counter.builder("litmus_client_fetch_experiment_polling_total_request") + .tag("vertical", vertical) + .tag("status", String.valueOf(response.code())) + .tag("experiment_state", LitmusExperimentResponse.Status.CHANGED.name()) + .tag("app_name", litmusConfig.getAppName()) + .register(this.litmusConfig.getMeterRegistry()) + .increment(); return new LitmusExperimentResponse(LitmusExperimentResponse.Status.CHANGED, experiments); - } else if(response.code() == 304) { + } else if (response.code() == 304) { + Counter.builder("litmus_client_fetch_experiment_polling_request_status") + .tag("vertical", vertical) + .tag("app_name", litmusConfig.getAppName()) + .tag("status", String.valueOf(response.code())) + .tag("experiment_state", LitmusExperimentResponse.Status.NOT_CHANGED.name()) + .register(this.litmusConfig.getMeterRegistry()) + .increment(); + return new LitmusExperimentResponse(LitmusExperimentResponse.Status.NOT_CHANGED, response.code()); } else { + Counter.builder("litmus_client_fetch_experiment_polling_request_status") + .tag("vertical", vertical) + .tag("app_name", litmusConfig.getAppName()) + .tag("status", String.valueOf(response.code())) + .tag("experiment_state", LitmusExperimentResponse.Status.NOT_CHANGED.name()) + .register(this.litmusConfig.getMeterRegistry()) + .increment(); + return new LitmusExperimentResponse(LitmusExperimentResponse.Status.UNAVAILABLE, response.code()); } } catch (Exception e) { + Counter.builder("litmus_client_fetch_experiment_polling_request_failed") + .tag("vertical", vertical) + .tag("app_name", litmusConfig.getAppName()) + .register(this.litmusConfig.getMeterRegistry()) + .increment(); throw new LitmusException("fetch experiments failed.", e); } } @@ -57,12 +93,26 @@ public class HttpExperimentFetcher implements ExperimentFetcher { .url(String.format("%s?segment_name=%s&id=%s", this.segmentIdUrl, segmentName, id)) .build(); - try(Response response = client.newCall(request).execute()) { + try (Response response = client.newCall(request).execute()) { var responseBody = response.body(); assert responseBody != null; + Counter.builder("litmus_client_segment_id_exist_request") + .tag("vertical", litmusConfig.getVertical()) + .tag("app_name", litmusConfig.getAppName()) + .tag("status", String.valueOf(response.code())) + .register(this.litmusConfig.getMeterRegistry()) + .increment(); return JacksonUtils.stringToObject(responseBody.string(), LitmusResponse.class); } catch (Exception e) { + Counter.builder("litmus_client_segment_id_exist_request_failed") + .tag("vertical", litmusConfig.getVertical()) + .tag("segment_name", segmentName) + .tag("app_name", litmusConfig.getAppName()) + .tag("exception", e.getMessage()) + .register(this.litmusConfig.getMeterRegistry()) + .increment(); + throw new LitmusException("segment name to id matching exists.", e); } } diff --git a/litmus-client/src/main/java/com/navi/medici/config/LitmusConfig.java b/litmus-client/src/main/java/com/navi/medici/config/LitmusConfig.java index a9901d6..2cd29e6 100644 --- a/litmus-client/src/main/java/com/navi/medici/config/LitmusConfig.java +++ b/litmus-client/src/main/java/com/navi/medici/config/LitmusConfig.java @@ -8,6 +8,7 @@ import com.navi.medici.scheduler.LitmusScheduledExecutorImpl; import com.navi.medici.strategy.Strategy; import com.navi.medici.strategy.UnknownStrategy; import com.navi.medici.util.LitmusURLs; +import io.micrometer.core.instrument.MeterRegistry; import java.io.File; import java.net.InetAddress; import java.net.URI; @@ -37,6 +38,8 @@ public class LitmusConfig { private final Strategy fallbackStrategy; private final LitmusExperimentBootstrapProvider litmusExperimentBootstrapProvider; private final String clickStreamAPI; + private final String vertical; + private final MeterRegistry meterRegistry; private LitmusConfig( @@ -52,7 +55,9 @@ public class LitmusConfig { LitmusScheduledExecutor litmusScheduledExecutor, Strategy fallbackStrategy, LitmusExperimentBootstrapProvider litmusBootstrapProvider, - String clickStreamAPI) { + String clickStreamAPI, + String vertical, + MeterRegistry meterRegistry) { if (appName == null) { throw new IllegalStateException("You are required to specify the litmus appName"); @@ -70,6 +75,19 @@ public class LitmusConfig { throw new IllegalStateException("You are required to specify a scheduler"); } + if (clickStreamAPI == null) { + throw new IllegalStateException("You are required to specify a scheduler"); + } + + if (vertical == null) { + throw new IllegalStateException("You are required to specify vertical"); + } + + if (meterRegistry == null) { + throw new IllegalStateException("You are required to specify meter registry"); + } + + this.fallbackStrategy = Objects.requireNonNullElseGet(fallbackStrategy, UnknownStrategy::new); this.litmusAPI = litmusAPI; @@ -85,6 +103,8 @@ public class LitmusConfig { this.litmusScheduledExecutor = litmusScheduledExecutor; this.litmusExperimentBootstrapProvider = litmusBootstrapProvider; this.clickStreamAPI = clickStreamAPI; + this.vertical = vertical; + this.meterRegistry = meterRegistry; } public static Builder builder() { @@ -150,6 +170,12 @@ public class LitmusConfig { return clickStreamAPI; } + public String getVertical() {return vertical;} + + public MeterRegistry getMeterRegistry() { + return meterRegistry; + } + public static class Builder { private URI litmusAPI; @@ -165,6 +191,8 @@ public class LitmusConfig { private @Nullable Strategy fallbackStrategy; private @Nullable LitmusExperimentBootstrapProvider litmusExperimentBootstrapProvider; private @Nullable String clickStreamAPI; + private String vertical; + private MeterRegistry meterRegistry; private static String getHostname() { String hostName = System.getProperty("hostname"); @@ -252,6 +280,17 @@ public class LitmusConfig { return this; } + public Builder vertical(String vertical) { + this.vertical = vertical; + return this; + } + + public Builder meterRegistry(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + return this; + } + + private String getBackupFile() { if (backupFile != null) { return backupFile; @@ -277,7 +316,9 @@ public class LitmusConfig { .orElseGet(LitmusScheduledExecutorImpl::getInstance), fallbackStrategy, litmusExperimentBootstrapProvider, - clickStreamAPI); + clickStreamAPI, + vertical, + meterRegistry); } public String getDefaultSdkVersion() { diff --git a/litmus-client/src/main/java/com/navi/medici/event/EventDispatcher.java b/litmus-client/src/main/java/com/navi/medici/event/EventDispatcher.java index 1dac79c..164ffe2 100644 --- a/litmus-client/src/main/java/com/navi/medici/event/EventDispatcher.java +++ b/litmus-client/src/main/java/com/navi/medici/event/EventDispatcher.java @@ -11,6 +11,7 @@ import com.navi.medici.request.v1.LitmusExperiment; import com.navi.medici.util.JacksonUtils; import com.navi.medici.variants.Variant; import java.util.List; +import java.util.concurrent.CompletableFuture; public class EventDispatcher { private final ClickStreamClient clickStreamClient; @@ -47,11 +48,11 @@ public class EventDispatcher { .customerReferenceId(litmusContext.getUserId().orElse("")) .build()); - clickStreamClient.publish(clickStreamPayload); + CompletableFuture.supplyAsync(() -> { + clickStreamClient.publish(litmusExperiment.getName(), clickStreamPayload); + return null; + }); + } } - - private void clickstreamIngestion() { - - } } diff --git a/litmus-client/src/main/java/com/navi/medici/litmus/DefaultLitmus.java b/litmus-client/src/main/java/com/navi/medici/litmus/DefaultLitmus.java index 9be9153..c839409 100644 --- a/litmus-client/src/main/java/com/navi/medici/litmus/DefaultLitmus.java +++ b/litmus-client/src/main/java/com/navi/medici/litmus/DefaultLitmus.java @@ -23,6 +23,8 @@ import com.navi.medici.strategy.UserWithIdStrategy; import com.navi.medici.util.JacksonUtils; import com.navi.medici.util.VariantUtil; import com.navi.medici.variants.Variant; + +import io.micrometer.core.instrument.Counter; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -33,9 +35,12 @@ import lombok.extern.log4j.Log4j2; @Log4j2 public class DefaultLitmus implements Litmus { + + public static final UnknownStrategy UNKNOWN_STRATEGY = new UnknownStrategy(); public static final Variant DISABLED_VARIANT = new Variant("disabled", null, false, null); + private final ExperimentRepository experimentRepository; private final Map strategyMap; private final LitmusContextProvider contextProvider; @@ -149,6 +154,12 @@ public class DefaultLitmus implements Litmus { }); } + Counter.builder("litmus_client_experiment_check_enabled") + .tag("experiment_name", experimentName) + .tag("result", String.valueOf(enabled)) + .register(this.config.getMeterRegistry()) + .increment(); + this.eventDispatcher.publish(context, litmusExperiment, enabled, null); log.info("experiment_name: {}, result: {}", experimentName, enabled); return enabled; @@ -204,6 +215,14 @@ public class DefaultLitmus implements Litmus { this.eventDispatcher.publish(context, litmusExperiment, enabled, variant); log.info("experiment_name: {}, result: {}", experimentName, enabled); + + Counter.builder("litmus_client_experiment_variant") + .tag("experiment_name", experimentName) + .tag("result", String.valueOf(enabled)) + .tag("variant", variant.getName()) + .register(this.config.getMeterRegistry()) + .increment(); + return variant; } @@ -217,4 +236,7 @@ public class DefaultLitmus implements Litmus { return getVariant(experimentName, contextProvider.getContext(), defaultValue); } + + + } diff --git a/litmus-client/src/main/java/com/navi/medici/repository/LitmusExperimentRepository.java b/litmus-client/src/main/java/com/navi/medici/repository/LitmusExperimentRepository.java index 591a9a7..4a839c6 100644 --- a/litmus-client/src/main/java/com/navi/medici/repository/LitmusExperimentRepository.java +++ b/litmus-client/src/main/java/com/navi/medici/repository/LitmusExperimentRepository.java @@ -51,18 +51,18 @@ public class LitmusExperimentRepository implements ExperimentRepository { } if (litmusConfig.isSynchronousFetchOnInitialisation()) { - updateExperiments().run(); + updateExperiments(litmusConfig.getVertical(), litmusConfig.getFetchLitmusExperimentsInterval()).run(); } this.inMemoryCache = new InMemoryCache(this.experimentCollection); - executor.setInterval(updateExperiments(), 0, litmusConfig.getFetchLitmusExperimentsInterval()); + executor.setInterval(updateExperiments(litmusConfig.getVertical(), litmusConfig.getFetchLitmusExperimentsInterval()), 0, litmusConfig.getFetchLitmusExperimentsInterval()); } - private Runnable updateExperiments() { + private Runnable updateExperiments(String vertical, Long pollingTime) { return () -> { try { - LitmusExperimentResponse response = experimentFetcher.fetchExperiments(); - if (response.getStatus() == LitmusExperimentResponse.Status.CHANGED) { + LitmusExperimentResponse response = experimentFetcher.fetchExperiments(vertical, pollingTime); + if (response != null && LitmusExperimentResponse.Status.CHANGED == response.getStatus()) { experimentCollection = response.getExperimentCollection(); experimentBackupHandler.write(response.getExperimentCollection()); this.inMemoryCache = new InMemoryCache(experimentCollection); diff --git a/litmus-client/src/main/java/com/navi/medici/strategy/DeviceWithIdStrategy.java b/litmus-client/src/main/java/com/navi/medici/strategy/DeviceWithIdStrategy.java index 14603af..8956e1e 100644 --- a/litmus-client/src/main/java/com/navi/medici/strategy/DeviceWithIdStrategy.java +++ b/litmus-client/src/main/java/com/navi/medici/strategy/DeviceWithIdStrategy.java @@ -1,9 +1,15 @@ package com.navi.medici.strategy; +import static com.navi.medici.strategy.FlexibleRolloutStrategy.GROUP_ID; +import static com.navi.medici.strategy.FlexibleRolloutStrategy.PERCENTAGE; + import com.navi.medici.client.HttpExperimentFetcher; import com.navi.medici.context.LitmusContext; +import com.navi.medici.util.StrategyUtils; import java.util.Map; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; public class DeviceWithIdStrategy implements Strategy { @@ -28,10 +34,30 @@ public class DeviceWithIdStrategy implements Strategy { @Override public boolean isEnabled(Map parameters, LitmusContext litmusContext) { + if (StringUtils.isBlank(parameters.get(PERCENTAGE))) { + return deviceWithIdSegmentCheck(parameters, litmusContext); + } else { + return segmentCheckWithRollout(parameters, litmusContext); + } + } + + private boolean deviceWithIdSegmentCheck(Map parameters, LitmusContext litmusContext) { + var deviceId = litmusContext.getDeviceId().orElse(null); var segmentName = parameters.get(SEGMENT); - var result = experimentFetcher.segmentIdExists(segmentName, litmusContext.getDeviceId().orElse(null)); + var result = experimentFetcher.segmentIdExists(segmentName, deviceId); return result.getData() != null && (Boolean) result.getData(); + } + private boolean segmentCheckWithRollout(Map parameters, LitmusContext litmusContext) { + var deviceId = litmusContext.getDeviceId().orElse(null); + var percentage = StrategyUtils.getPercentage(parameters.get(PERCENTAGE)); + var groupId = parameters.getOrDefault(GROUP_ID, ""); + + var norm = StrategyUtils.getNormalizedNumber(deviceId, groupId); + + return Optional.of(deviceWithIdSegmentCheck(parameters, litmusContext)) + .map(r -> r && percentage > 0 && norm <= percentage) + .orElse(false); } } diff --git a/litmus-client/src/main/java/com/navi/medici/strategy/FlexibleRolloutStrategy.java b/litmus-client/src/main/java/com/navi/medici/strategy/FlexibleRolloutStrategy.java index f2e8974..f862e6f 100644 --- a/litmus-client/src/main/java/com/navi/medici/strategy/FlexibleRolloutStrategy.java +++ b/litmus-client/src/main/java/com/navi/medici/strategy/FlexibleRolloutStrategy.java @@ -15,13 +15,6 @@ public class FlexibleRolloutStrategy implements Strategy { protected static final String PERCENTAGE = "rollout"; protected static final String GROUP_ID = "groupId"; - private final Supplier randomGenerator; - - public FlexibleRolloutStrategy() { - this.randomGenerator = () -> Math.random() * 100 + ""; - } - - @Override public String getName() { return "flexibleRollout"; @@ -32,32 +25,12 @@ public class FlexibleRolloutStrategy implements Strategy { return false; } - private Optional resolveStickiness(String stickiness, LitmusContext context) { - switch (stickiness) { - case "userId": - return context.getUserId(); - case "sessionId": - return context.getSessionId(); - case "deviceId": - return context.getDeviceId(); - case "appVersionCode": - return context.getAppVersionCode(); - case "osType": - return context.getOsType(); - case "random": - return Optional.of(randomGenerator.get()); - case "default": - return Optional.of(context.getUserId() - .orElse(context.getSessionId().orElse(this.randomGenerator.get()))); - default: - return context.getByName(stickiness); - } - } + @Override public boolean isEnabled(Map parameters, LitmusContext litmusContext) { final String stickiness = getStickiness(parameters); - final Optional stickinessId = resolveStickiness(stickiness, litmusContext); + Optional stickinessId = StrategyUtils.resolveStickiness(stickiness, litmusContext); final int percentage = StrategyUtils.getPercentage(parameters.get(PERCENTAGE)); final String groupId = parameters.getOrDefault(GROUP_ID, ""); diff --git a/litmus-client/src/main/java/com/navi/medici/strategy/UserWithIdStrategy.java b/litmus-client/src/main/java/com/navi/medici/strategy/UserWithIdStrategy.java index b25953e..2e4348d 100644 --- a/litmus-client/src/main/java/com/navi/medici/strategy/UserWithIdStrategy.java +++ b/litmus-client/src/main/java/com/navi/medici/strategy/UserWithIdStrategy.java @@ -1,9 +1,15 @@ package com.navi.medici.strategy; +import static com.navi.medici.strategy.FlexibleRolloutStrategy.GROUP_ID; +import static com.navi.medici.strategy.FlexibleRolloutStrategy.PERCENTAGE; + import com.navi.medici.client.HttpExperimentFetcher; import com.navi.medici.context.LitmusContext; +import com.navi.medici.util.StrategyUtils; import java.util.Map; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; public class UserWithIdStrategy implements Strategy { @@ -28,10 +34,31 @@ public class UserWithIdStrategy implements Strategy { @Override public boolean isEnabled(Map parameters, LitmusContext litmusContext) { + if (StringUtils.isBlank(parameters.get(PERCENTAGE))) { + return userWithIdSegmentCheck(parameters, litmusContext); + } else { + return segmentCheckWithRollout(parameters, litmusContext); + } + } + + private boolean userWithIdSegmentCheck(Map parameters, LitmusContext litmusContext) { + var userId = litmusContext.getUserId().orElse(null); var segmentName = parameters.get(SEGMENT); - var result = experimentFetcher.segmentIdExists(segmentName, litmusContext.getUserId().orElse(null)); + var result = experimentFetcher.segmentIdExists(segmentName, userId); return result.getData() != null && (Boolean) result.getData(); } + private boolean segmentCheckWithRollout(Map parameters, LitmusContext litmusContext) { + var userId = litmusContext.getUserId().orElse(null); + var percentage = StrategyUtils.getPercentage(parameters.get(PERCENTAGE)); + var groupId = parameters.getOrDefault(GROUP_ID, ""); + + var norm = StrategyUtils.getNormalizedNumber(userId, groupId); + + return Optional.of(userWithIdSegmentCheck(parameters, litmusContext)) + .map(r -> r && percentage > 0 && norm <= percentage) + .orElse(false); + } + } diff --git a/litmus-client/src/main/java/com/navi/medici/util/StrategyUtils.java b/litmus-client/src/main/java/com/navi/medici/util/StrategyUtils.java index 57f57f2..634dc36 100644 --- a/litmus-client/src/main/java/com/navi/medici/util/StrategyUtils.java +++ b/litmus-client/src/main/java/com/navi/medici/util/StrategyUtils.java @@ -1,7 +1,10 @@ package com.navi.medici.util; import com.navi.medici.annotation.Nullable; +import com.navi.medici.context.LitmusContext; import com.sangupta.murmur.Murmur3; +import java.util.Optional; +import java.util.function.Supplier; public class StrategyUtils { @@ -47,4 +50,27 @@ public class StrategyUtils { } } + public static Optional resolveStickiness(String stickiness, LitmusContext context) { + Supplier randomGenerator = () -> Math.random() * 100 + ""; + switch (stickiness) { + case "userId": + return context.getUserId(); + case "sessionId": + return context.getSessionId(); + case "deviceId": + return context.getDeviceId(); + case "appVersionCode": + return context.getAppVersionCode(); + case "osType": + return context.getOsType(); + case "random": + return Optional.of(randomGenerator.get()); + case "default": + return Optional.of(context.getUserId() + .orElse(context.getSessionId().orElse(randomGenerator.get()))); + default: + return context.getByName(stickiness); + } + } + } diff --git a/litmus-core/src/main/java/com/navi/medici/controller/v1/ExperimentController.java b/litmus-core/src/main/java/com/navi/medici/controller/v1/ExperimentController.java index bed7c75..222177e 100644 --- a/litmus-core/src/main/java/com/navi/medici/controller/v1/ExperimentController.java +++ b/litmus-core/src/main/java/com/navi/medici/controller/v1/ExperimentController.java @@ -8,13 +8,16 @@ import io.micrometer.core.annotation.Timed; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -37,6 +40,19 @@ public class ExperimentController { return experimentService.fetchAllExperiments(); } + @GetMapping(value = "/vertical") + @Timed(value = "litmus.fetch.vertical.experiment", percentiles = {0.95, 0.99}) + public ResponseEntity fetchExperimentsForAVertical(@RequestParam("vertical") String vertical, + @RequestParam("polling_time") Long pollingTime) { + + var collection = experimentService.fetchAllExperimentsForVerticals(vertical, pollingTime); + if (collection.getLitmusExperiments().isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_MODIFIED).body(collection); + } + + return ResponseEntity.ok(collection); + } + @PutMapping(value = "/attach/variants/{experiment_id}", consumes = MediaType.APPLICATION_JSON_VALUE) @Timed(value = "litmus.attach.variants", percentiles = {0.95, 0.99}) public void attachVariants(@PathVariable("experiment_id") String experimentId, 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 66738eb..2d35089 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 @@ -6,9 +6,12 @@ import com.navi.medici.variants.VariantDefinition; import java.util.List; public interface ExperimentService { + void create(LitmusExperiment litmusExperimentRequest); LitmusExperimentCollection fetchAllExperiments(); void attachVariants(String experimentId, List variantDefinitions); + + LitmusExperimentCollection fetchAllExperimentsForVerticals(String vertical, Long pollingTime); } 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 b6691bd..f0f9fb4 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 @@ -1,5 +1,8 @@ package com.navi.medici.service.experiment; +import static com.navi.medici.enums.ExperimentType.KILL_SWITCH; +import static com.navi.medici.enums.ExperimentType.RELEASE; + import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.exceptions.BadRequestException; import com.navi.medici.query.experiment.IExperimentQuery; @@ -41,9 +44,16 @@ public record ExperimentServiceImpl(IExperimentQuery experimentQuery, @Override public LitmusExperimentCollection fetchAllExperiments() { List experimentEntities = experimentQuery.findByEnabled(true); + List litmusExperiments = experimentEntities.stream() .map(this::build) - .collect(Collectors.toList()); + .peek(le -> { + //This is to reduce sending strategy string over network + if (RELEASE.equals(le.getType()) || KILL_SWITCH.equals(le.getType())) { + le.setStrategies(null); + } + }).collect(Collectors.toList()); + return LitmusExperimentCollection.builder() .litmusExperiments(litmusExperiments) .build(); @@ -63,6 +73,19 @@ public record ExperimentServiceImpl(IExperimentQuery experimentQuery, experimentQuery.save(experiment); } + @Override + public LitmusExperimentCollection fetchAllExperimentsForVerticals(String vertical, Long pollingTime) { + var experimentEntities = experimentQuery.findByVertical(vertical, pollingTime); + + List litmusExperiments = experimentEntities.stream() + .map(this::build) + .collect(Collectors.toList()); + + return LitmusExperimentCollection.builder() + .litmusExperiments(litmusExperiments) + .build(); + } + private LitmusExperiment build(ExperimentEntity experimentEntity) { return LitmusExperiment.builder() .experimentId(experimentEntity.getExperimentId()) @@ -75,6 +98,7 @@ public record ExperimentServiceImpl(IExperimentQuery experimentQuery, .type(experimentEntity.getType()) .startTime(experimentEntity.getStartTime()) .endTime(experimentEntity.getEndTime()) + .vertical(experimentEntity.getVertical()) .build(); } } diff --git a/litmus-core/src/main/resources/application.properties b/litmus-core/src/main/resources/application.properties index a6f8a9d..c722155 100644 --- a/litmus-core/src/main/resources/application.properties +++ b/litmus-core/src/main/resources/application.properties @@ -14,7 +14,7 @@ spring.jpa.hibernate.ddl-auto=none #spring.jpa.properties.hibernate.use_sql_comments=true #spring.jpa.properties.hibernate.format_sql=true -management.server.port=4001 +management.server.port=4000 management.endpoints.web.exposure.include=prometheus,health,info,metric,heapdump,threaddump server.tomcat.mbeanregistry.enabled=true spring.jmx.enabled=true 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 2ea3845..3ba52fe 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 @@ -65,6 +65,9 @@ public class ExperimentEntity { @Column(name = "end_time") LocalDateTime endTime; + @Column(name = "vertical") + String vertical; + @Version private Integer version; 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 910bf9c..658c4db 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,6 +2,7 @@ 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; @@ -10,6 +11,7 @@ import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class ExperimentQueryImpl implements IExperimentQuery { + private final ExperimentRepository experimentRepository; @Override @@ -27,6 +29,13 @@ public class ExperimentQueryImpl implements IExperimentQuery { return experimentRepository.findByExperimentId(experimentId); } + @Override + public List findByVertical(String vertical, Long pollingTime) { + var lastPollingTime = LocalDateTime.now().minusSeconds(pollingTime); + + return experimentRepository.findByVerticalAndUpdatedTime(vertical, lastPollingTime, LocalDateTime.now()); + } + @Override public void save(ExperimentEntity experiment) { experimentRepository.save(experiment); 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 0a05d53..a6b260c 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 @@ -11,5 +11,7 @@ public interface IExperimentQuery { Optional findByExperimentId(String experimentId); + List findByVertical(String vertical, Long pollingTime); + void save(ExperimentEntity experiment); } diff --git a/litmus-db/src/main/java/com/navi/medici/repository/ExperimentRepository.java b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentRepository.java index 141d2d1..2169d31 100644 --- a/litmus-db/src/main/java/com/navi/medici/repository/ExperimentRepository.java +++ b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentRepository.java @@ -1,16 +1,26 @@ package com.navi.medici.repository; import com.navi.medici.entity.ExperimentEntity; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface ExperimentRepository extends CrudRepository { + List findByEnabled(Boolean enabled); Optional findByName(String name); Optional findByExperimentId(String experimentId); + + @Query(value = "select * from experiments e where e.vertical = :vertical and e.updated_at >= :lastPollingTime and e.updated_at <= :currentTime ", + nativeQuery = true) + List findByVerticalAndUpdatedTime(@Param("vertical") String vertical, + @Param("lastPollingTime") LocalDateTime lastPollingTime, + @Param("currentTime") LocalDateTime currentTime); } diff --git a/litmus-liquibase/src/main/resources/db/changelog/202204011642-add-vertical-column.sql b/litmus-liquibase/src/main/resources/db/changelog/202204011642-add-vertical-column.sql new file mode 100644 index 0000000..6e213e5 --- /dev/null +++ b/litmus-liquibase/src/main/resources/db/changelog/202204011642-add-vertical-column.sql @@ -0,0 +1,6 @@ +--liquibase formatted sql + +--changeset author:chandresh id:202204011642 + +ALTER TABLE experiments +ADD COLUMN vertical VARCHAR; \ No newline at end of file diff --git a/litmus-mock/pom.xml b/litmus-mock/pom.xml index 38311c8..899759f 100644 --- a/litmus-mock/pom.xml +++ b/litmus-mock/pom.xml @@ -27,7 +27,18 @@ org.springframework.boot - spring-boot-starter-webflux + spring-boot-starter-actuator + + + com.squareup.okhttp3 + okhttp + + + + + + org.springframework.boot + spring-boot-starter-web org.springframework.boot @@ -36,6 +47,18 @@ + + io.micrometer + micrometer-registry-prometheus + 1.8.4 + + + + bouncycastle + bcprov-jdk16 + 140 + + javax.servlet javax.servlet-api diff --git a/litmus-mock/src/main/java/com/navi/medici/container/MockContainer.java b/litmus-mock/src/main/java/com/navi/medici/container/MockContainer.java index 7b39e5a..dff6cb2 100644 --- a/litmus-mock/src/main/java/com/navi/medici/container/MockContainer.java +++ b/litmus-mock/src/main/java/com/navi/medici/container/MockContainer.java @@ -3,6 +3,7 @@ package com.navi.medici.container; import com.navi.medici.config.LitmusConfig; import com.navi.medici.litmus.DefaultLitmus; import com.navi.medici.litmus.Litmus; +import io.micrometer.core.instrument.MeterRegistry; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @@ -10,13 +11,15 @@ import org.springframework.stereotype.Component; public class MockContainer { @Bean - public Litmus litmus() { + public Litmus litmus(MeterRegistry meterRegistry) { var litmusConfig = LitmusConfig.builder() .litmusAPI("http://localhost:12000/litmus-core/v1") .appName("litmus-mock") .instanceId("test-instance") .litmusContextProvider(new CustomLitmusContextProvider()) .clickStreamAPI("https://dev-janus.np.navi-tech.in/events/json") + .vertical("PL") + .meterRegistry(meterRegistry) .build(); Litmus litmus = new DefaultLitmus(litmusConfig); diff --git a/litmus-mock/src/main/resources/application.properties b/litmus-mock/src/main/resources/application.properties index e97e724..a689ab2 100644 --- a/litmus-mock/src/main/resources/application.properties +++ b/litmus-mock/src/main/resources/application.properties @@ -1 +1,4 @@ -server.port=11000 \ No newline at end of file +server.port=11000 + +management.server.port=4001 +management.endpoints.web.exposure.include=prometheus,health,info,metric,heapdump,threaddump \ No newline at end of file diff --git a/litmus-proxy/pom.xml b/litmus-proxy/pom.xml index b56c5a8..a43f228 100644 --- a/litmus-proxy/pom.xml +++ b/litmus-proxy/pom.xml @@ -31,6 +31,8 @@ spring-boot-starter-web + + org.springframework.boot spring-boot-starter-actuator diff --git a/litmus-proxy/src/main/java/com/navi/medici/container/LitmusProxyContainer.java b/litmus-proxy/src/main/java/com/navi/medici/container/LitmusProxyContainer.java index af5975d..a58f241 100644 --- a/litmus-proxy/src/main/java/com/navi/medici/container/LitmusProxyContainer.java +++ b/litmus-proxy/src/main/java/com/navi/medici/container/LitmusProxyContainer.java @@ -20,6 +20,7 @@ public record LitmusProxyContainer(LitmusProxyConfig litmusProxyConfig, LitmusCo .instanceId("test-instance") .litmusContextProvider(litmusContextProvider) .clickStreamAPI(litmusProxyConfig.getClickStreamEndpoint()) + .vertical("PL") .build(); Litmus litmus = new DefaultLitmus(litmusConfig);