From 48d8d94dbe51913ea1c062a497cc2f349478112d Mon Sep 17 00:00:00 2001 From: chandresh pancholi Date: Thu, 21 Oct 2021 20:53:01 +0530 Subject: [PATCH] variant integration with litmus Signed-off-by: chandresh pancholi --- Dockerfile.core | 18 ++ Dockerfile.proxy | 17 ++ .../LitmusExperimentBootstrapHandler.java | 4 +- .../navi/medici/client/ClickStreamClient.java | 45 +++++ .../client/ExperimentBackupHandlerFile.java | 10 +- .../medici/client/HttpExperimentFetcher.java | 8 +- .../com/navi/medici/config/LitmusConfig.java | 19 +- .../navi/medici/event/EventDispatcher.java | 51 +++++ .../com/navi/medici/litmus/DefaultLitmus.java | 48 ++++- .../java/com/navi/medici/litmus/Litmus.java | 10 + .../com/navi/medici/util/JacksonUtils.java | 29 ++- .../com/navi/medici/util/VariantUtil.java | 130 +++++++++++++ litmus-core/pom.xml | 20 +- litmus-liquibase/pom.xml | 11 -- litmus-mock/pom.xml | 6 - .../src/main/java/com/navi/medici/App.java | 13 -- .../CustomLitmusContextProvider.java | 5 +- .../navi/medici/container/MockContainer.java | 12 +- .../medici/controller/MockController.java | 4 +- .../medici/clickstream/ClickStreamEvent.java | 28 +++ .../clickstream/ClickStreamPayload.java | 174 ++++++++++++++++++ .../clickstream/LitmusExperimentEvent.java | 25 +++ .../navi/medici/context/LitmusContext.java | 46 +++-- .../event/LitmusExperimentEvaluated.java | 23 +++ .../medici/interceptor/RequestMetadata.java | 83 +++++++++ .../medici/strategy/ActivationStrategy.java | 2 + .../com/navi/medici/variants/Payload.java | 22 +++ .../com/navi/medici/variants/Variant.java | 24 +++ .../medici/variants/VariantDefinition.java | 26 +++ .../navi/medici/variants/VariantOverride.java | 23 +++ litmus-proxy/pom.xml | 34 +++- .../src/main/java/com/navi/medici/App.java | 13 -- .../java/com/navi/medici/LitmusProxyApp.java | 12 ++ .../navi/medici/config/LitmusProxyConfig.java | 8 + .../container/LitmusProxyContainer.java | 27 +++ .../controller/LitmusProxyController.java | 27 +++ .../medici/filter/RequestMetadataHandler.java | 61 ++++++ .../CustomLitmusProxyContextProvider.java | 28 +++ .../src/main/resources/application.properties | 20 ++ .../src/main/resources/log4j2.properties | 9 + pom.xml | 8 +- 41 files changed, 1081 insertions(+), 102 deletions(-) create mode 100644 Dockerfile.core create mode 100644 Dockerfile.proxy create mode 100644 litmus-client/src/main/java/com/navi/medici/client/ClickStreamClient.java create mode 100644 litmus-client/src/main/java/com/navi/medici/event/EventDispatcher.java create mode 100644 litmus-client/src/main/java/com/navi/medici/util/VariantUtil.java delete mode 100644 litmus-mock/src/main/java/com/navi/medici/App.java create mode 100644 litmus-model/src/main/java/com/navi/medici/clickstream/ClickStreamEvent.java create mode 100644 litmus-model/src/main/java/com/navi/medici/clickstream/ClickStreamPayload.java create mode 100644 litmus-model/src/main/java/com/navi/medici/clickstream/LitmusExperimentEvent.java create mode 100644 litmus-model/src/main/java/com/navi/medici/event/LitmusExperimentEvaluated.java create mode 100644 litmus-model/src/main/java/com/navi/medici/interceptor/RequestMetadata.java create mode 100644 litmus-model/src/main/java/com/navi/medici/variants/Payload.java create mode 100644 litmus-model/src/main/java/com/navi/medici/variants/Variant.java create mode 100644 litmus-model/src/main/java/com/navi/medici/variants/VariantDefinition.java create mode 100644 litmus-model/src/main/java/com/navi/medici/variants/VariantOverride.java delete mode 100644 litmus-proxy/src/main/java/com/navi/medici/App.java create mode 100644 litmus-proxy/src/main/java/com/navi/medici/LitmusProxyApp.java create mode 100644 litmus-proxy/src/main/java/com/navi/medici/config/LitmusProxyConfig.java create mode 100644 litmus-proxy/src/main/java/com/navi/medici/container/LitmusProxyContainer.java create mode 100644 litmus-proxy/src/main/java/com/navi/medici/controller/LitmusProxyController.java create mode 100644 litmus-proxy/src/main/java/com/navi/medici/filter/RequestMetadataHandler.java create mode 100644 litmus-proxy/src/main/java/com/navi/medici/provider/CustomLitmusProxyContextProvider.java create mode 100644 litmus-proxy/src/main/resources/application.properties create mode 100644 litmus-proxy/src/main/resources/log4j2.properties diff --git a/Dockerfile.core b/Dockerfile.core new file mode 100644 index 0000000..7b45ea9 --- /dev/null +++ b/Dockerfile.core @@ -0,0 +1,18 @@ +FROM 193044292705.dkr.ecr.ap-south-1.amazonaws.com/common/spring-boot-maven:1.0 as builder +ARG ARTIFACT_VERSION=0.0.1-SNAPSHOT +RUN mkdir -p /build +WORKDIR /build +COPY . /build +RUN mvn -B dependency:resolve dependency:resolve-plugins +RUN mvn clean verify -DskipTests -Dartifact.version=${ARTIFACT_VERSION} + +FROM 193044292705.dkr.ecr.ap-south-1.amazonaws.com/common/openjdk:17-slim-bullseye +ARG ARTIFACT_VERSION=0.0.1-SNAPSHOT +RUN mkdir -p /usr/local +RUN apt-get update -y && apt-get -y install fontconfig libpng-dev +WORKDIR /usr/local/ +COPY --from=0 /build/litmus-core/target/litmus-core-${ARTIFACT_VERSION}.jar /usr/local/litmus-core.jar +COPY --from=0 /build/litmus-liquibase/target/litmus-liquibase-${ARTIFACT_VERSION}.jar /usr/local/database.jar +RUN adduser --system --uid 4000 --disabled-password app-user && chown -R 4000:4000 /usr/local && chmod -R g+w /usr/local +USER 4000 +CMD java ${JVM_OPTS} -jar /usr/local/litmus-core.jar \ No newline at end of file diff --git a/Dockerfile.proxy b/Dockerfile.proxy new file mode 100644 index 0000000..9b8185d --- /dev/null +++ b/Dockerfile.proxy @@ -0,0 +1,17 @@ +FROM 193044292705.dkr.ecr.ap-south-1.amazonaws.com/common/spring-boot-maven:1.0 as builder +ARG ARTIFACT_VERSION=0.0.1-SNAPSHOT +RUN mkdir -p /build +WORKDIR /build +COPY . /build +RUN mvn -B dependency:resolve dependency:resolve-plugins +RUN mvn clean verify -DskipTests -Dartifact.version=${ARTIFACT_VERSION} + +FROM 193044292705.dkr.ecr.ap-south-1.amazonaws.com/common/openjdk:17-slim-bullseye +ARG ARTIFACT_VERSION=0.0.1-SNAPSHOT +RUN mkdir -p /usr/local +RUN apt-get update -y && apt-get -y install fontconfig libpng-dev +WORKDIR /usr/local/ +COPY --from=0 /build/litmus-core/target/litmus-proxy-${ARTIFACT_VERSION}.jar /usr/local/litmus-proxy.jar +RUN adduser --system --uid 4000 --disabled-password app-user && chown -R 4000:4000 /usr/local && chmod -R g+w /usr/local +USER 4000 +CMD java ${JVM_OPTS} -jar /usr/local/litmus-proxy.jar \ No newline at end of file diff --git a/litmus-client/src/main/java/com/navi/medici/bootstrap/LitmusExperimentBootstrapHandler.java b/litmus-client/src/main/java/com/navi/medici/bootstrap/LitmusExperimentBootstrapHandler.java index 80a296e..c351a02 100644 --- a/litmus-client/src/main/java/com/navi/medici/bootstrap/LitmusExperimentBootstrapHandler.java +++ b/litmus-client/src/main/java/com/navi/medici/bootstrap/LitmusExperimentBootstrapHandler.java @@ -12,7 +12,6 @@ import lombok.extern.log4j.Log4j2; public class LitmusExperimentBootstrapHandler { private final LitmusExperimentBootstrapProvider litmusExperimentBootstrapProvider; - private final JacksonUtils jacksonUtils; public LitmusExperimentBootstrapHandler(LitmusConfig litmusConfig) { if (litmusConfig.getLitmusExperimentBootstrapProvider() != null) { @@ -20,12 +19,11 @@ public class LitmusExperimentBootstrapHandler { } else { this.litmusExperimentBootstrapProvider = new LitmusExperimentBootstrapFileProvider(); } - this.jacksonUtils = new JacksonUtils(); } public LitmusExperimentCollection parse(@Nullable String jsonString) { if (jsonString != null) { - return jacksonUtils.stringToObject(jsonString, LitmusExperimentCollection.class); + return JacksonUtils.stringToObject(jsonString, LitmusExperimentCollection.class); } return LitmusExperimentCollection.builder() .litmusExperiments(Collections.emptyList()) 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 new file mode 100644 index 0000000..cdfa3c1 --- /dev/null +++ b/litmus-client/src/main/java/com/navi/medici/client/ClickStreamClient.java @@ -0,0 +1,45 @@ +package com.navi.medici.client; + + +import com.navi.medici.clickstream.ClickStreamPayload; +import com.navi.medici.config.LitmusConfig; +import com.navi.medici.util.JacksonUtils; +import lombok.extern.log4j.Log4j2; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +@Log4j2 +public class ClickStreamClient { + public static final MediaType JSON + = MediaType.parse("application/json; charset=utf-8"); + + private final LitmusConfig litmusConfig; + private final OkHttpClient client; + + public ClickStreamClient(LitmusConfig litmusConfig) { + this.litmusConfig = litmusConfig; + this.client = new OkHttpClient(); + } + + public void publish(ClickStreamPayload payload) { + String requestBody = JacksonUtils.objectToString(payload); + Request request = new Request.Builder() + .url(this.litmusConfig.getClickStreamAPI()) + .post(RequestBody.create(JSON, requestBody)) + .build(); + + try(Response response = client.newCall(request).execute()) { + if (response.code() < 300) { + var responseBody = response.body(); + assert responseBody != null; + + log.debug("experiment event ingested."); + } + } catch (Exception e) { + log.error("clickstream event ingestion failed. ", e); + } + } +} diff --git a/litmus-client/src/main/java/com/navi/medici/client/ExperimentBackupHandlerFile.java b/litmus-client/src/main/java/com/navi/medici/client/ExperimentBackupHandlerFile.java index 014c967..0b0fbcc 100644 --- a/litmus-client/src/main/java/com/navi/medici/client/ExperimentBackupHandlerFile.java +++ b/litmus-client/src/main/java/com/navi/medici/client/ExperimentBackupHandlerFile.java @@ -19,25 +19,21 @@ import org.apache.logging.log4j.core.util.IOUtils; public class ExperimentBackupHandlerFile implements ExperimentBackupHandler { private final String backupFile; - private final JacksonUtils jacksonUtils; public ExperimentBackupHandlerFile(LitmusConfig config) { this.backupFile = config.getBackupFile(); - this.jacksonUtils = new JacksonUtils(); } @Override public LitmusExperimentCollection read() { log.info("Litmus will try to load experiments states from temporary backup"); try (FileReader reader = new FileReader(backupFile)) { - LitmusExperimentCollection experimentCollection = jacksonUtils.stringToObject(IOUtils.toString(reader), LitmusExperimentCollection.class); - return experimentCollection; + return JacksonUtils.stringToObject(IOUtils.toString(reader), LitmusExperimentCollection.class); } catch (FileNotFoundException e) { log.info( " Litmus could not find the backup-file '" + backupFile - + "'. \n" - + "This is expected behavior the first time litmus runs in a new environment."); + + ". This is expected behavior the first time litmus runs in a new environment."); } catch (IOException | IllegalStateException e) { log.error(""); } @@ -50,7 +46,7 @@ public class ExperimentBackupHandlerFile implements ExperimentBackupHandler { @Override public void write(LitmusExperimentCollection experimentCollection) { try (FileWriter writer = new FileWriter(backupFile)) { - writer.write(jacksonUtils.objectToString(experimentCollection)); + writer.write(JacksonUtils.objectToString(experimentCollection)); } catch (IOException e) { throw new LitmusException( "Litmus was unable to backup experiments to file: " + backupFile, e); 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 0e3ad48..a2b6b8d 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 @@ -18,7 +18,6 @@ public class HttpExperimentFetcher implements ExperimentFetcher { private final OkHttpClient client; private final URL experimentUrl; private final URL segmentIdUrl; - private final JacksonUtils jacksonUtils; public HttpExperimentFetcher(LitmusConfig litmusConfig) { this.client = new OkHttpClient(); @@ -26,7 +25,6 @@ public class HttpExperimentFetcher implements ExperimentFetcher { litmusConfig.getLitmusURLs() .getLitmusExperimentsURL(litmusConfig.getProjectName(), litmusConfig.getNamePrefix()); this.segmentIdUrl = litmusConfig.getLitmusURLs().getSegmentIdURL(); - this.jacksonUtils = new JacksonUtils(); } @Override @@ -40,7 +38,7 @@ public class HttpExperimentFetcher implements ExperimentFetcher { var responseBody = response.body(); assert responseBody != null; - LitmusExperimentCollection experiments = jacksonUtils.stringToObject(responseBody.string(), LitmusExperimentCollection.class); + LitmusExperimentCollection experiments = JacksonUtils.stringToObject(responseBody.string(), LitmusExperimentCollection.class); return new LitmusExperimentResponse(LitmusExperimentResponse.Status.CHANGED, experiments); } else if(response.code() == 304) { @@ -54,7 +52,7 @@ public class HttpExperimentFetcher implements ExperimentFetcher { } } - public LitmusResponse segmentIdExists(String segmentName, String id) { + public LitmusResponse segmentIdExists(String segmentName, String id) { Request request = new Request.Builder() .url(String.format("%s?segment_name=%s&id=%s", this.segmentIdUrl, segmentName, id)) .build(); @@ -63,7 +61,7 @@ public class HttpExperimentFetcher implements ExperimentFetcher { var responseBody = response.body(); assert responseBody != null; - return jacksonUtils.stringToObject(responseBody.string(), LitmusResponse.class); + return JacksonUtils.stringToObject(responseBody.string(), LitmusResponse.class); } catch (Exception e) { 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 c1af2bb..a9901d6 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 @@ -36,6 +36,7 @@ public class LitmusConfig { private final LitmusScheduledExecutor litmusScheduledExecutor; private final Strategy fallbackStrategy; private final LitmusExperimentBootstrapProvider litmusExperimentBootstrapProvider; + private final String clickStreamAPI; private LitmusConfig( @@ -50,7 +51,8 @@ public class LitmusConfig { boolean synchronousFetchOnInitialisation, LitmusScheduledExecutor litmusScheduledExecutor, Strategy fallbackStrategy, - LitmusExperimentBootstrapProvider litmusBootstrapProvider) { + LitmusExperimentBootstrapProvider litmusBootstrapProvider, + String clickStreamAPI) { if (appName == null) { throw new IllegalStateException("You are required to specify the litmus appName"); @@ -82,6 +84,7 @@ public class LitmusConfig { this.synchronousFetchOnInitialisation = synchronousFetchOnInitialisation; this.litmusScheduledExecutor = litmusScheduledExecutor; this.litmusExperimentBootstrapProvider = litmusBootstrapProvider; + this.clickStreamAPI = clickStreamAPI; } public static Builder builder() { @@ -143,6 +146,10 @@ public class LitmusConfig { return litmusExperimentBootstrapProvider; } + public String getClickStreamAPI() { + return clickStreamAPI; + } + public static class Builder { private URI litmusAPI; @@ -157,6 +164,7 @@ public class LitmusConfig { private @Nullable LitmusScheduledExecutor scheduledExecutor; private @Nullable Strategy fallbackStrategy; private @Nullable LitmusExperimentBootstrapProvider litmusExperimentBootstrapProvider; + private @Nullable String clickStreamAPI; private static String getHostname() { String hostName = System.getProperty("hostname"); @@ -239,6 +247,11 @@ public class LitmusConfig { return this; } + public Builder clickStreamAPI(@Nullable String clickStreamAPI) { + this.clickStreamAPI = clickStreamAPI; + return this; + } + private String getBackupFile() { if (backupFile != null) { return backupFile; @@ -248,6 +261,7 @@ public class LitmusConfig { } } + public LitmusConfig build() { return new LitmusConfig( litmusAPI, @@ -262,7 +276,8 @@ public class LitmusConfig { Optional.ofNullable(scheduledExecutor) .orElseGet(LitmusScheduledExecutorImpl::getInstance), fallbackStrategy, - litmusExperimentBootstrapProvider); + litmusExperimentBootstrapProvider, + clickStreamAPI); } 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 new file mode 100644 index 0000000..611dc99 --- /dev/null +++ b/litmus-client/src/main/java/com/navi/medici/event/EventDispatcher.java @@ -0,0 +1,51 @@ +package com.navi.medici.event; + +import com.navi.medici.clickstream.ClickStreamEvent; +import com.navi.medici.clickstream.ClickStreamPayload; +import com.navi.medici.clickstream.ClickStreamPayload.User; +import com.navi.medici.clickstream.LitmusExperimentEvent; +import com.navi.medici.client.ClickStreamClient; +import com.navi.medici.config.LitmusConfig; +import com.navi.medici.context.LitmusContext; +import com.navi.medici.request.v1.LitmusExperiment; +import com.navi.medici.util.JacksonUtils; +import java.util.List; + +public class EventDispatcher { + private final ClickStreamClient clickStreamClient; + private final JacksonUtils jacksonUtils; + + public EventDispatcher(LitmusConfig litmusConfig) { + this.clickStreamClient = new ClickStreamClient(litmusConfig); + } + + public void publish(LitmusContext litmusContext, LitmusExperiment litmusExperiment, Boolean result) { + var clickStreamPayloadOptional = litmusContext.getClickStreamPayload(); + + var clickStreamPayload = clickStreamPayloadOptional + .map(payload -> JacksonUtils.stringToObject(payload, ClickStreamPayload.class)) + .orElse(null); + + if (clickStreamPayload != null) { + var litmusExperimentEvent = LitmusExperimentEvent.builder() + .experimentId(litmusExperiment.getExperimentId()) + .experimentName(litmusExperiment.getName()) + .result(result) + .build(); + + var event = ClickStreamEvent.builder() + .eventName("litmus-experiment-event") + .timestamp(System.currentTimeMillis()) + .eventPayload(litmusExperimentEvent) + .build(); + + clickStreamPayload.setClickStreamEvent(List.of(event)); + clickStreamPayload.setSource("Litmus"); + clickStreamPayload.setUser(User.builder() + .customerReferenceId(litmusContext.getUserId().orElse("")) + .build()); + + clickStreamClient.publish(clickStreamPayload); + } + } +} 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 c92a061..46edd0b 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 @@ -3,10 +3,12 @@ package com.navi.medici.litmus; import static java.util.Optional.ofNullable; import com.navi.medici.annotation.Nullable; +import com.navi.medici.client.ClickStreamClient; import com.navi.medici.client.ExperimentBackupHandlerFile; import com.navi.medici.client.HttpExperimentFetcher; import com.navi.medici.config.LitmusConfig; import com.navi.medici.context.LitmusContext; +import com.navi.medici.event.EventDispatcher; import com.navi.medici.provider.LitmusContextProvider; import com.navi.medici.repository.ExperimentRepository; import com.navi.medici.repository.LitmusExperimentRepository; @@ -17,6 +19,8 @@ import com.navi.medici.strategy.FlexibleRolloutStrategy; import com.navi.medici.strategy.Strategy; import com.navi.medici.strategy.UnknownStrategy; import com.navi.medici.strategy.UserWithIdStrategy; +import com.navi.medici.util.VariantUtil; +import com.navi.medici.variants.Variant; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -28,11 +32,13 @@ 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; private final LitmusConfig config; + private final EventDispatcher eventDispatcher; private static ExperimentRepository defaultExperimentRepository(LitmusConfig litmusConfig) { return new LitmusExperimentRepository( @@ -54,7 +60,8 @@ public class DefaultLitmus implements Litmus { litmusConfig, experimentRepository, buildStrategyMap(strategies, litmusConfig), - litmusConfig.getContextProvider()); + litmusConfig.getContextProvider(), + new EventDispatcher(litmusConfig)); } // Visible for testing @@ -62,11 +69,13 @@ public class DefaultLitmus implements Litmus { LitmusConfig litmusConfig, ExperimentRepository experimentRepository, Map strategyMap, - LitmusContextProvider contextProvider) { + LitmusContextProvider contextProvider, + EventDispatcher eventDispatcher) { this.config = litmusConfig; this.experimentRepository = experimentRepository; this.strategyMap = strategyMap; this.contextProvider = contextProvider; + this.eventDispatcher = eventDispatcher; } @Override @@ -98,6 +107,9 @@ public class DefaultLitmus implements Litmus { LitmusContext context, BiFunction fallbackAction) { boolean enabled = checkEnabled(experimentName, context, fallbackAction); + + + //Todo clickstream integration return enabled; } @@ -127,12 +139,16 @@ public class DefaultLitmus implements Litmus { experimentName, strategy.getName()); } - return configuredStrategy.isEnabled( + var result = configuredStrategy.isEnabled( strategy.getParameters(), context, strategy.getConstraints()); + this.eventDispatcher.publish(context, litmusExperiment, result); + + return result; }); } + return enabled; } @@ -170,4 +186,30 @@ public class DefaultLitmus implements Litmus { config.getScheduledExecutor().shutdown(); } + @Override + public Variant getVariant(String experimentName, LitmusContext context) { + return getVariant(experimentName, context, DISABLED_VARIANT); + } + + @Override + public Variant getVariant(String experimentName, LitmusContext context, Variant defaultValue) { + LitmusExperiment litmusExperiment = experimentRepository.getLitmusExperiment(experimentName); + boolean enabled = checkEnabled(experimentName, context, (n, c) -> false); + Variant variant = + enabled + ? VariantUtil.selectVariant(litmusExperiment, context, defaultValue) + : defaultValue; + return variant; + } + + @Override + public Variant getVariant(String experimentName) { + return getVariant(experimentName, contextProvider.getContext()); + } + + @Override + public Variant getVariant(String experimentName, Variant defaultValue) { + return getVariant(experimentName, contextProvider.getContext(), defaultValue); + } + } diff --git a/litmus-client/src/main/java/com/navi/medici/litmus/Litmus.java b/litmus-client/src/main/java/com/navi/medici/litmus/Litmus.java index ad6445c..85830f2 100644 --- a/litmus-client/src/main/java/com/navi/medici/litmus/Litmus.java +++ b/litmus-client/src/main/java/com/navi/medici/litmus/Litmus.java @@ -1,6 +1,7 @@ package com.navi.medici.litmus; import com.navi.medici.context.LitmusContext; +import com.navi.medici.variants.Variant; import java.util.function.BiFunction; public interface Litmus { @@ -31,4 +32,13 @@ public interface Litmus { default void shutdown() { } + + Variant getVariant(final String experimentName, final LitmusContext context); + + Variant getVariant( + final String experimentName, final LitmusContext context, final Variant defaultValue); + + Variant getVariant(final String experimentName); + + Variant getVariant(final String experimentName, final Variant defaultValue); } diff --git a/litmus-client/src/main/java/com/navi/medici/util/JacksonUtils.java b/litmus-client/src/main/java/com/navi/medici/util/JacksonUtils.java index b67119e..1f4c75d 100644 --- a/litmus-client/src/main/java/com/navi/medici/util/JacksonUtils.java +++ b/litmus-client/src/main/java/com/navi/medici/util/JacksonUtils.java @@ -2,29 +2,38 @@ package com.navi.medici.util; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.CollectionType; +import java.util.ArrayList; +import java.util.List; import lombok.extern.log4j.Log4j2; @Log4j2 public class JacksonUtils { - private final ObjectMapper objectMapper; - - public JacksonUtils() { - this.objectMapper = new ObjectMapper(); - } - - public String objectToString(Object o) { + public static String objectToString(Object o) { try { - return objectMapper.writeValueAsString(o); + return new ObjectMapper().writeValueAsString(o); } catch (JsonProcessingException e) { throw new RuntimeException("object to string conversion failed", e); } } - public T stringToObject(String s, Class klazz) { + public static T stringToObject(String s, Class klazz) { try { - return objectMapper.readValue(s, klazz); + return new ObjectMapper().readValue(s, klazz); } catch (JsonProcessingException e) { throw new RuntimeException("string to object conversion failed", e); } } + + public static List stringToListObject(String s, Class klazz) { + try { + CollectionType listType = new ObjectMapper().getTypeFactory().constructCollectionType(ArrayList.class, klazz); + + return new ObjectMapper().readValue(s, listType); + } catch (JsonProcessingException e) { + throw new RuntimeException("string to list object conversion failed", e); + } + } + + } \ No newline at end of file diff --git a/litmus-client/src/main/java/com/navi/medici/util/VariantUtil.java b/litmus-client/src/main/java/com/navi/medici/util/VariantUtil.java new file mode 100644 index 0000000..d2068db --- /dev/null +++ b/litmus-client/src/main/java/com/navi/medici/util/VariantUtil.java @@ -0,0 +1,130 @@ +package com.navi.medici.util; + +import com.navi.medici.context.LitmusContext; +import com.navi.medici.request.v1.LitmusExperiment; +import com.navi.medici.variants.Variant; +import com.navi.medici.variants.VariantDefinition; +import com.navi.medici.variants.VariantOverride; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.function.Predicate; + +public final class VariantUtil { + private VariantUtil() {} + + private static Predicate overrideMatchesContext(LitmusContext context) { + return (override) -> { + Optional contextValue; + switch (override.getContextName()) { + case "userId": + contextValue = context.getUserId(); + break; + case "sessionId": + contextValue = context.getSessionId(); + break; + case "remoteAddress": + contextValue = context.getRemoteAddress(); + break; + case "appVersionCode": + contextValue = context.getAppVersionCode(); + break; + case "osType": + contextValue = context.getOsType(); + break; + case "deviceId": + contextValue = context.getDeviceId(); + break; + default: + contextValue = + Optional.ofNullable( + context.getProperties().get(override.getContextName())); + break; + } + return override.getValues().contains(contextValue.orElse("")); + }; + } + + private static Optional getOverride( + List variants, LitmusContext context) { + return variants.stream() + .filter( + variant -> + variant.getOverrides().stream() + .anyMatch(overrideMatchesContext(context))) + .findFirst(); + } + + private static String getIdentifier(LitmusContext context) { + return context.getUserId() + .orElse( + context.getSessionId() + .orElse( + context.getRemoteAddress() + .orElse(context.getAppVersionCode() + .orElse(context.getOsType() + .orElse(context.getDeviceId() + .orElse(Double.toString(Math.random()))))) + )); + } + + private static String randomString() { + int randSeed = new Random().nextInt(100000); + return "" + randSeed; + } + + private static String getSeed(LitmusContext litmusContext, Optional stickiness) { + return stickiness + .map(s -> litmusContext.getByName(s).orElse(randomString())) + .orElse(getIdentifier(litmusContext)); + } + + public static Variant selectVariant(LitmusExperiment litmusExperiment, LitmusContext context, Variant defaultVariant) { + if (litmusExperiment == null) { + return defaultVariant; + } + List variants = JacksonUtils.stringToListObject(litmusExperiment.getVariants(), VariantDefinition.class); + int totalWeight = variants.stream().mapToInt(VariantDefinition::getWeight).sum(); + if (totalWeight == 0) { + return defaultVariant; + } + + Optional variantOverride = getOverride(variants, context); + if (variantOverride.isPresent()) { + var variantDefinition = variantOverride.get(); + return Variant.builder() + .name(variantDefinition.getName()) + .payload(variantDefinition.getPayload()) + .enabled(true) + .stickiness(variantDefinition.getStickiness()) + .build(); + } + Optional customStickiness = + variants.stream() + .filter(f -> f.getStickiness() != null) + .map(VariantDefinition::getStickiness) + .findFirst(); + int target = + StrategyUtils.getNormalizedNumber( + getSeed(context, customStickiness), litmusExperiment.getName(), totalWeight); + + int counter = 0; + for (final VariantDefinition definition : variants) { + if (definition.getWeight() != 0) { + counter += definition.getWeight(); + if (counter >= target) { + return Variant.builder() + .name(definition.getName()) + .payload(definition.getPayload()) + .enabled(true) + .stickiness(definition.getStickiness()) + .build(); + } + } + } + + // Should not happen + return defaultVariant; + } + +} diff --git a/litmus-core/pom.xml b/litmus-core/pom.xml index 0e1ae04..37263a3 100644 --- a/litmus-core/pom.xml +++ b/litmus-core/pom.xml @@ -63,13 +63,27 @@ 2.17.51 - - com.opencsv opencsv 5.5.2 - + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + + diff --git a/litmus-liquibase/pom.xml b/litmus-liquibase/pom.xml index e925257..e8a5662 100644 --- a/litmus-liquibase/pom.xml +++ b/litmus-liquibase/pom.xml @@ -45,17 +45,6 @@ org.apache.maven.plugins maven-compiler-plugin 3.8.1 - - 11 - 11 - - - org.projectlombok - lombok - 1.18.20 - - - diff --git a/litmus-mock/pom.xml b/litmus-mock/pom.xml index db0bb11..07260bd 100644 --- a/litmus-mock/pom.xml +++ b/litmus-mock/pom.xml @@ -42,12 +42,6 @@ 4.0.1 - - junit - junit - 4.11 - test - diff --git a/litmus-mock/src/main/java/com/navi/medici/App.java b/litmus-mock/src/main/java/com/navi/medici/App.java deleted file mode 100644 index c0d56af..0000000 --- a/litmus-mock/src/main/java/com/navi/medici/App.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.navi.medici; - -/** - * Hello world! - * - */ -public class App -{ - public static void main( String[] args ) - { - System.out.println( "Hello World!" ); - } -} diff --git a/litmus-mock/src/main/java/com/navi/medici/container/CustomLitmusContextProvider.java b/litmus-mock/src/main/java/com/navi/medici/container/CustomLitmusContextProvider.java index b3e7588..d17b56c 100644 --- a/litmus-mock/src/main/java/com/navi/medici/container/CustomLitmusContextProvider.java +++ b/litmus-mock/src/main/java/com/navi/medici/container/CustomLitmusContextProvider.java @@ -2,7 +2,6 @@ package com.navi.medici.container; import com.navi.medici.context.LitmusContext; import com.navi.medici.provider.LitmusContextProvider; -import java.util.Random; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Component; import org.springframework.web.context.annotation.RequestScope; @@ -15,7 +14,9 @@ public class CustomLitmusContextProvider implements LitmusContextProvider { @Override public LitmusContext getContext() { return LitmusContext.builder() - .userId("test1") + .clickStreamPayload(null) + .userId("test123") + .build(); } } \ No newline at end of file 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 e7a84e2..f5e5512 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 @@ -12,15 +12,15 @@ public class MockContainer { @Bean public Litmus litmus() { var litmusConfig = LitmusConfig.builder() - .litmusAPI("http://localhost:12000/v1") - .appName("litmus-mock") - .instanceId("test-instance") + .litmusAPI("http://localhost:12000/v1") + .appName("litmus-mock") + .instanceId("test-instance") .litmusContextProvider(new CustomLitmusContextProvider()) - .build(); + .build(); - Litmus unleash = new DefaultLitmus(litmusConfig); + Litmus litmus = new DefaultLitmus(litmusConfig); - return unleash; + return litmus; } } diff --git a/litmus-mock/src/main/java/com/navi/medici/controller/MockController.java b/litmus-mock/src/main/java/com/navi/medici/controller/MockController.java index 9e4c8eb..029785c 100644 --- a/litmus-mock/src/main/java/com/navi/medici/controller/MockController.java +++ b/litmus-mock/src/main/java/com/navi/medici/controller/MockController.java @@ -1,5 +1,6 @@ package com.navi.medici.controller; +import com.navi.medici.context.LitmusContext; import com.navi.medici.litmus.Litmus; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -17,7 +18,8 @@ public class MockController { @GetMapping("/mock") public String test(@RequestParam("experiment") String experiment) { - var result = litmus.isEnabled(experiment); + var context = LitmusContext.builder().addProperty("cc", "5").build(); + var result = litmus.isEnabled(experiment, context); log.info("result ----> {}", result); return "result ==> " + result; diff --git a/litmus-model/src/main/java/com/navi/medici/clickstream/ClickStreamEvent.java b/litmus-model/src/main/java/com/navi/medici/clickstream/ClickStreamEvent.java new file mode 100644 index 0000000..efe3461 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/clickstream/ClickStreamEvent.java @@ -0,0 +1,28 @@ +package com.navi.medici.clickstream; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ClickStreamEvent { + @JsonProperty("attributes") + T eventPayload; + + @JsonProperty("event_name") + String eventName; + + @JsonProperty("timestamp") + Long timestamp; +} + diff --git a/litmus-model/src/main/java/com/navi/medici/clickstream/ClickStreamPayload.java b/litmus-model/src/main/java/com/navi/medici/clickstream/ClickStreamPayload.java new file mode 100644 index 0000000..38326d1 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/clickstream/ClickStreamPayload.java @@ -0,0 +1,174 @@ +package com.navi.medici.clickstream; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ClickStreamPayload { + @JsonProperty("app") + App app; + + @JsonProperty("device") + Device device; + + @JsonProperty("os") + Os os; + + @JsonProperty("location") + Location location; + + @JsonProperty("network") + Network network; + + @JsonProperty("session") + Session session; + + @JsonProperty("user") + User user; + + @JsonProperty("source") + String source; + + @JsonProperty("metadata") + Map metadata; + + @JsonProperty("events") + List> clickStreamEvent; + + @NoArgsConstructor + @AllArgsConstructor + @Getter + @Builder + @JsonIgnoreProperties(ignoreUnknown = true) + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class App { + @JsonProperty("name") + String name; + + @JsonProperty("version") + String version; + + @JsonProperty("version_name") + String versionName; + } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + @Builder + @JsonIgnoreProperties(ignoreUnknown = true) + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class Device { + @JsonProperty("device_id") + String deviceId; + + @JsonProperty("advertising_id") + String advertisingId; + + @JsonProperty("manufacturer") + String manufacturer; + + @JsonProperty("model") + String model; + + @JsonProperty("os") + String os; + + @JsonProperty("os_version") + String osVersion; + } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + @Builder + @JsonIgnoreProperties(ignoreUnknown = true) + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class Os { + @JsonProperty("name") + String name; + + @JsonProperty("version") + String version; + + @JsonProperty("api_version") + String apiVersion; + } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + @Builder + @JsonIgnoreProperties(ignoreUnknown = true) + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class Location { + @JsonProperty("latitude") + String latitude; + + @JsonProperty("longitude") + String longitude; + } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + @Builder + @JsonIgnoreProperties(ignoreUnknown = true) + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class Network { + @JsonProperty("carrier") + String carrier; + + @JsonProperty("ip") + String ip; + + @JsonProperty("type") + String type; + } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + @Builder + @JsonIgnoreProperties(ignoreUnknown = true) + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class Session { + @JsonProperty("session_id") + String sessionId; + + @JsonProperty("start_time") + Long startTime; + + @JsonProperty("referral_url") + String referralUrl; + } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + @Builder + @JsonIgnoreProperties(ignoreUnknown = true) + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class User { + @JsonProperty("customer_id") + String customerReferenceId; + } + + +} + diff --git a/litmus-model/src/main/java/com/navi/medici/clickstream/LitmusExperimentEvent.java b/litmus-model/src/main/java/com/navi/medici/clickstream/LitmusExperimentEvent.java new file mode 100644 index 0000000..c83748a --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/clickstream/LitmusExperimentEvent.java @@ -0,0 +1,25 @@ +package com.navi.medici.clickstream; + +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; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +public class LitmusExperimentEvent { + String experimentId; + + String experimentName; + + Boolean result; +} diff --git a/litmus-model/src/main/java/com/navi/medici/context/LitmusContext.java b/litmus-model/src/main/java/com/navi/medici/context/LitmusContext.java index bd537d3..1cbbd62 100644 --- a/litmus-model/src/main/java/com/navi/medici/context/LitmusContext.java +++ b/litmus-model/src/main/java/com/navi/medici/context/LitmusContext.java @@ -1,6 +1,7 @@ package com.navi.medici.context; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.navi.medici.clickstream.ClickStreamPayload; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -19,6 +20,7 @@ import lombok.experimental.FieldDefaults; @JsonIgnoreProperties(ignoreUnknown = true) @FieldDefaults(level = AccessLevel.PRIVATE) public class LitmusContext { + Optional appName; Optional environment; Optional userId; @@ -28,6 +30,8 @@ public class LitmusContext { Optional osType; Optional deviceId; Map properties; + Optional clickStreamPayload; + public Optional getByName(String contextName) { switch (contextName) { @@ -57,18 +61,30 @@ public class LitmusContext { } public static class Builder { - @Nullable private String appName; - @Nullable private String environment; - @Nullable private String userId; - @Nullable private String sessionId; - @Nullable private String remoteAddress; - @Nullable private String appVersionCode; - @Nullable private String osType; - @Nullable private String deviceId; + + @Nullable + private String appName; + @Nullable + private String environment; + @Nullable + private String userId; + @Nullable + private String sessionId; + @Nullable + private String remoteAddress; + @Nullable + private String appVersionCode; + @Nullable + private String osType; + @Nullable + private String deviceId; + @Nullable + private String clickStreamPayload; private final Map properties = new HashMap<>(); - public Builder() {} + public Builder() { + } public Builder(LitmusContext context) { context.appName.ifPresent(val -> this.appName = val); @@ -79,6 +95,7 @@ public class LitmusContext { context.appVersionCode.ifPresent(val -> this.appVersionCode = val); context.osType.ifPresent(val -> this.osType = val); context.deviceId.ifPresent(val -> this.deviceId = val); + context.clickStreamPayload.ifPresent(val -> this.clickStreamPayload = val); this.properties.putAll(context.properties); } @@ -127,11 +144,16 @@ public class LitmusContext { return this; } + public Builder clickStreamPayload(String clickStreamPayload) { + this.clickStreamPayload = clickStreamPayload; + return this; + } + public LitmusContext build() { return new LitmusContext( - Optional.ofNullable(environment), Optional.ofNullable(appName),Optional.ofNullable(userId), - Optional.ofNullable(sessionId), Optional.ofNullable( remoteAddress), Optional.ofNullable( appVersionCode), - Optional.ofNullable(osType), Optional.ofNullable(deviceId), properties); + Optional.ofNullable(environment), Optional.ofNullable(appName), Optional.ofNullable(userId), + Optional.ofNullable(sessionId), Optional.ofNullable(remoteAddress), Optional.ofNullable(appVersionCode), + Optional.ofNullable(osType), Optional.ofNullable(deviceId), properties, Optional.ofNullable(clickStreamPayload)); } } diff --git a/litmus-model/src/main/java/com/navi/medici/event/LitmusExperimentEvaluated.java b/litmus-model/src/main/java/com/navi/medici/event/LitmusExperimentEvaluated.java new file mode 100644 index 0000000..947d5a9 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/event/LitmusExperimentEvaluated.java @@ -0,0 +1,23 @@ +package com.navi.medici.event; + + +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; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +public class LitmusExperimentEvaluated { + String experimentName; + boolean enabled; +} diff --git a/litmus-model/src/main/java/com/navi/medici/interceptor/RequestMetadata.java b/litmus-model/src/main/java/com/navi/medici/interceptor/RequestMetadata.java new file mode 100644 index 0000000..e0d9b33 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/interceptor/RequestMetadata.java @@ -0,0 +1,83 @@ +package com.navi.medici.interceptor; + +import java.util.Optional; +import java.util.UUID; +import org.apache.logging.log4j.ThreadContext; +import org.springframework.stereotype.Component; + +@Component +public class RequestMetadata { + + public static final String MESSAGE = "X-Reply-Message"; + private static final String CORRELATION_ID = "correlationId"; + public static final String CUSTOMER_ID_HEADER = "X-Medici-Customer-Id"; + public static final String CUSTOMER_ID = "customerId"; + public static final String REQUEST_TYPE_HEADER = "X-Request-Type"; + public static final String STATUS = "X-Reply-Status"; + public static final String APP_VERSION_CODE = "appVersionCode"; + public static final String OS_VERSION = "osVersion"; + public static final String X_CLICK_STREAM_DATA = "X-Click-Stream-Data"; + + public String correlationId() { + return ThreadContext.get(CORRELATION_ID); + } + + public String getClickStreamData() { + return ThreadContext.get(X_CLICK_STREAM_DATA); + } + + public void updateClickStreamData(String clickStreamData) { + ThreadContext.put(X_CLICK_STREAM_DATA, clickStreamData); + } + + public Optional customerId() { + return Optional.ofNullable(ThreadContext.get(CUSTOMER_ID)); + } + + public String generateCorrelationId() { + return UUID.randomUUID().toString(); + } + + + public void updateAppVersionCode(String appVersionCode) { + ThreadContext.put(APP_VERSION_CODE, appVersionCode); + } + + public Optional getAppVersionCode() { + return Optional.ofNullable(ThreadContext.get(APP_VERSION_CODE)); + } + + public void resetAppVersionCode() { + ThreadContext.remove(APP_VERSION_CODE); + } + + public void updateOSVersionCode(String osVersionCode) { + ThreadContext.put(OS_VERSION, osVersionCode); + } + + public Optional getOsVersion() { + return Optional.ofNullable(ThreadContext.get(OS_VERSION)); + } + + public void resetOsVersion() { + ThreadContext.remove(OS_VERSION); + } + + public void updateCorrelationId(String requestId) { + ThreadContext.put(CORRELATION_ID, requestId); + } + + public void resetCorrelationId() { + ThreadContext.remove(CORRELATION_ID); + } + + public void updateCustomerId(String customerId) { + ThreadContext.put(CUSTOMER_ID, customerId); + } + + public void resetCustomerId() { + ThreadContext.remove(CUSTOMER_ID); + } + + public void resetClickStreamData() {ThreadContext.remove(X_CLICK_STREAM_DATA);} +} diff --git a/litmus-model/src/main/java/com/navi/medici/strategy/ActivationStrategy.java b/litmus-model/src/main/java/com/navi/medici/strategy/ActivationStrategy.java index b2f584d..2a13154 100644 --- a/litmus-model/src/main/java/com/navi/medici/strategy/ActivationStrategy.java +++ b/litmus-model/src/main/java/com/navi/medici/strategy/ActivationStrategy.java @@ -2,6 +2,7 @@ package com.navi.medici.strategy; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.navi.medici.constraint.Constraint; +import com.navi.medici.variants.VariantDefinition; import java.util.Collections; import java.util.List; import java.util.Map; @@ -25,5 +26,6 @@ public class ActivationStrategy { String name; Map parameters; List constraints = Collections.emptyList(); + List variants; } diff --git a/litmus-model/src/main/java/com/navi/medici/variants/Payload.java b/litmus-model/src/main/java/com/navi/medici/variants/Payload.java new file mode 100644 index 0000000..135f75f --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/variants/Payload.java @@ -0,0 +1,22 @@ +package com.navi.medici.variants; + +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; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Payload { + String type; + String value; +} diff --git a/litmus-model/src/main/java/com/navi/medici/variants/Variant.java b/litmus-model/src/main/java/com/navi/medici/variants/Variant.java new file mode 100644 index 0000000..02b60c1 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/variants/Variant.java @@ -0,0 +1,24 @@ +package com.navi.medici.variants; + +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; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Variant { + String name; + Payload payload; + boolean enabled; + String stickiness; +} diff --git a/litmus-model/src/main/java/com/navi/medici/variants/VariantDefinition.java b/litmus-model/src/main/java/com/navi/medici/variants/VariantDefinition.java new file mode 100644 index 0000000..8b277db --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/variants/VariantDefinition.java @@ -0,0 +1,26 @@ +package com.navi.medici.variants; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +public class VariantDefinition { + String name; + int weight; + Payload payload; + List overrides; + String stickiness; +} diff --git a/litmus-model/src/main/java/com/navi/medici/variants/VariantOverride.java b/litmus-model/src/main/java/com/navi/medici/variants/VariantOverride.java new file mode 100644 index 0000000..2db5037 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/variants/VariantOverride.java @@ -0,0 +1,23 @@ +package com.navi.medici.variants; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +public class VariantOverride { + String contextName; + List values; +} diff --git a/litmus-proxy/pom.xml b/litmus-proxy/pom.xml index 25f2445..de1cd8d 100644 --- a/litmus-proxy/pom.xml +++ b/litmus-proxy/pom.xml @@ -13,11 +13,37 @@ litmus-proxy + - junit - junit - 4.11 - test + com.navi.medici + litmus-model + 1.0-SNAPSHOT + + + + com.navi.medici + litmus-client + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + diff --git a/litmus-proxy/src/main/java/com/navi/medici/App.java b/litmus-proxy/src/main/java/com/navi/medici/App.java deleted file mode 100644 index c0d56af..0000000 --- a/litmus-proxy/src/main/java/com/navi/medici/App.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.navi.medici; - -/** - * Hello world! - * - */ -public class App -{ - public static void main( String[] args ) - { - System.out.println( "Hello World!" ); - } -} diff --git a/litmus-proxy/src/main/java/com/navi/medici/LitmusProxyApp.java b/litmus-proxy/src/main/java/com/navi/medici/LitmusProxyApp.java new file mode 100644 index 0000000..b146f0c --- /dev/null +++ b/litmus-proxy/src/main/java/com/navi/medici/LitmusProxyApp.java @@ -0,0 +1,12 @@ +package com.navi.medici; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LitmusProxyApp { + + public static void main(String[] args) { + SpringApplication.run(LitmusProxyApp.class, args); + } +} diff --git a/litmus-proxy/src/main/java/com/navi/medici/config/LitmusProxyConfig.java b/litmus-proxy/src/main/java/com/navi/medici/config/LitmusProxyConfig.java new file mode 100644 index 0000000..b84166d --- /dev/null +++ b/litmus-proxy/src/main/java/com/navi/medici/config/LitmusProxyConfig.java @@ -0,0 +1,8 @@ +package com.navi.medici.config; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class LitmusProxyConfig { + +} 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 new file mode 100644 index 0000000..9e9592a --- /dev/null +++ b/litmus-proxy/src/main/java/com/navi/medici/container/LitmusProxyContainer.java @@ -0,0 +1,27 @@ +package com.navi.medici.container; + +import com.navi.medici.config.LitmusConfig; +import com.navi.medici.interceptor.RequestMetadata; +import com.navi.medici.litmus.DefaultLitmus; +import com.navi.medici.litmus.Litmus; +import com.navi.medici.provider.CustomLitmusProxyContextProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +@Component +public class LitmusProxyContainer { + + @Bean + public Litmus litmus(RequestMetadata requestMetadata) { + var litmusConfig = LitmusConfig.builder() + .litmusAPI("http://localhost:12000/v1") + .appName("litmus-proxy") + .instanceId("test-instance") + .litmusContextProvider(new CustomLitmusProxyContextProvider(requestMetadata)) + .build(); + + Litmus litmus = new DefaultLitmus(litmusConfig); + + return litmus; + } +} diff --git a/litmus-proxy/src/main/java/com/navi/medici/controller/LitmusProxyController.java b/litmus-proxy/src/main/java/com/navi/medici/controller/LitmusProxyController.java new file mode 100644 index 0000000..efc84ff --- /dev/null +++ b/litmus-proxy/src/main/java/com/navi/medici/controller/LitmusProxyController.java @@ -0,0 +1,27 @@ +package com.navi.medici.controller; + +import com.navi.medici.litmus.Litmus; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v1/proxy") +@RequiredArgsConstructor +@Log4j2 +public class LitmusProxyController { + private final Litmus litmus; + + @GetMapping(value = "/experiment", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity fetch(@RequestParam("name") String experimentName) { + var result = litmus.isEnabled(experimentName); + + return ResponseEntity.ok(result); + } + +} diff --git a/litmus-proxy/src/main/java/com/navi/medici/filter/RequestMetadataHandler.java b/litmus-proxy/src/main/java/com/navi/medici/filter/RequestMetadataHandler.java new file mode 100644 index 0000000..ee0ba9f --- /dev/null +++ b/litmus-proxy/src/main/java/com/navi/medici/filter/RequestMetadataHandler.java @@ -0,0 +1,61 @@ +package com.navi.medici.filter; + +import com.navi.medici.interceptor.RequestMetadata; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.log4j.Log4j2; +import org.apache.logging.log4j.util.Strings; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +@Component +@Log4j2 +public class RequestMetadataHandler extends HandlerInterceptorAdapter { + + public static final String X_CORRELATION_ID = "X-Correlation-Id"; + public static final String APP_VERSION_CODE = "appVersionCode"; + public static final String OS_VERSION = "osVersion"; + public static final String X_CLICK_STREAM_DATA = "X-Click-Stream-Data"; + + private final RequestMetadata requestMetadata; + + @Autowired + public RequestMetadataHandler( + RequestMetadata requestMetadata) { + this.requestMetadata = requestMetadata; + } + + @Override + public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, + final Object handler) { + requestMetadata.resetCorrelationId(); + requestMetadata.resetCustomerId(); + requestMetadata.resetAppVersionCode(); + requestMetadata.resetOsVersion(); + requestMetadata.resetClickStreamData(); + String correlationId = request.getHeader(X_CORRELATION_ID); + String appVersionCode = request.getHeader(APP_VERSION_CODE); + String osVersion = request.getHeader(OS_VERSION); + requestMetadata.updateCorrelationId(Optional.ofNullable(correlationId) + .filter(Strings::isNotBlank).orElseGet(requestMetadata::generateCorrelationId)); + Optional.ofNullable(appVersionCode).ifPresent(requestMetadata::updateAppVersionCode); + Optional.ofNullable(osVersion).ifPresent(requestMetadata::updateOSVersionCode); + + String clickStreamData = request.getHeader(X_CLICK_STREAM_DATA); + Optional.ofNullable(clickStreamData).ifPresent(requestMetadata::updateClickStreamData); + + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) { + requestMetadata.resetCorrelationId(); + requestMetadata.resetCustomerId(); + requestMetadata.resetAppVersionCode(); + requestMetadata.resetOsVersion(); + } +} \ No newline at end of file diff --git a/litmus-proxy/src/main/java/com/navi/medici/provider/CustomLitmusProxyContextProvider.java b/litmus-proxy/src/main/java/com/navi/medici/provider/CustomLitmusProxyContextProvider.java new file mode 100644 index 0000000..b493317 --- /dev/null +++ b/litmus-proxy/src/main/java/com/navi/medici/provider/CustomLitmusProxyContextProvider.java @@ -0,0 +1,28 @@ +package com.navi.medici.provider; + +import com.navi.medici.context.LitmusContext; +import com.navi.medici.interceptor.RequestMetadata; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +@Log4j2 +public class CustomLitmusProxyContextProvider implements LitmusContextProvider { + + private final RequestMetadata requestMetadata; + + public CustomLitmusProxyContextProvider(RequestMetadata requestMetadata) { + this.requestMetadata = requestMetadata; + } + + @Override + public LitmusContext getContext() { + return LitmusContext.builder() + .clickStreamPayload(requestMetadata.getClickStreamData()) + .appVersionCode(requestMetadata.getAppVersionCode().orElse("")) + .osType(requestMetadata.getOsVersion().orElse("")) + .build(); + } +} diff --git a/litmus-proxy/src/main/resources/application.properties b/litmus-proxy/src/main/resources/application.properties new file mode 100644 index 0000000..2bfc9fd --- /dev/null +++ b/litmus-proxy/src/main/resources/application.properties @@ -0,0 +1,20 @@ +server.port=13000 + +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} +spring.datasource.url=${DATASOURCE_URL:jdbc:postgresql://localhost:5432/litmus}?stringtype=unspecified +spring.datasource.username=${DATASOURCE_USERNAME:postgres} +spring.datasource.password=${DATASOURCE_PASSWORD:admin} +spring.datasource.initialization-mode=${DATA_INITIALIZATION_MODE:never} +spring.jpa.hibernate.ddl-auto=none +#spring.jpa.properties.hibernate.generate_statistics]=true +#spring.jpa.properties.hibernate.show_sql=true +#spring.jpa.properties.hibernate.use_sql_comments=true +#spring.jpa.properties.hibernate.format_sql=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} \ No newline at end of file diff --git a/litmus-proxy/src/main/resources/log4j2.properties b/litmus-proxy/src/main/resources/log4j2.properties new file mode 100644 index 0000000..fb42297 --- /dev/null +++ b/litmus-proxy/src/main/resources/log4j2.properties @@ -0,0 +1,9 @@ +status=error +appenders=console +appender.console.type=Console +appender.console.name=STDOUT +appender.console.layout.type=EcsLayout +appender.console.layout.serviceName=litmus-proxy +appender.console.layout.topLevelLabels=correlationId,customerId +rootLogger.level=info +rootLogger.appenderRef.stdout.ref=STDOUT diff --git a/pom.xml b/pom.xml index 285b77f..e27c4cd 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,13 @@ 4.13.2 test - + + + org.apache.logging.log4j + log4j-api + 2.14.1 + +