variant integration with litmus
Signed-off-by: chandresh pancholi <chandresh.pancholi@navi.com>
This commit is contained in:
18
Dockerfile.core
Normal file
18
Dockerfile.core
Normal file
@@ -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
|
||||
17
Dockerfile.proxy
Normal file
17
Dockerfile.proxy
Normal file
@@ -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
|
||||
@@ -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())
|
||||
|
||||
@@ -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 <T> void publish(ClickStreamPayload<T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Boolean> 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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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.<LitmusExperimentEvent>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, Strategy> 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<String, Strategy> 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<String, LitmusContext, Boolean> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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> T stringToObject(String s, Class<T> klazz) {
|
||||
public static <T> T stringToObject(String s, Class<T> 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 <T> List<T> stringToListObject(String s, Class<T> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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<VariantOverride> overrideMatchesContext(LitmusContext context) {
|
||||
return (override) -> {
|
||||
Optional<String> 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<VariantDefinition> getOverride(
|
||||
List<VariantDefinition> 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<String> 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<VariantDefinition> variants = JacksonUtils.stringToListObject(litmusExperiment.getVariants(), VariantDefinition.class);
|
||||
int totalWeight = variants.stream().mapToInt(VariantDefinition::getWeight).sum();
|
||||
if (totalWeight == 0) {
|
||||
return defaultVariant;
|
||||
}
|
||||
|
||||
Optional<VariantDefinition> 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<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,13 +63,27 @@
|
||||
<version>2.17.51</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>com.opencsv</groupId>
|
||||
<artifactId>opencsv</artifactId>
|
||||
<version>5.5.2</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -45,17 +45,6 @@
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.20</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
@@ -42,12 +42,6 @@
|
||||
<version>4.0.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.11</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.navi.medici;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*
|
||||
*/
|
||||
public class App
|
||||
{
|
||||
public static void main( String[] args )
|
||||
{
|
||||
System.out.println( "Hello World!" );
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<T> {
|
||||
@JsonProperty("attributes")
|
||||
T eventPayload;
|
||||
|
||||
@JsonProperty("event_name")
|
||||
String eventName;
|
||||
|
||||
@JsonProperty("timestamp")
|
||||
Long timestamp;
|
||||
}
|
||||
|
||||
@@ -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<T> {
|
||||
@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<String, String> metadata;
|
||||
|
||||
@JsonProperty("events")
|
||||
List<ClickStreamEvent<T>> 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<String> appName;
|
||||
Optional<String> environment;
|
||||
Optional<String> userId;
|
||||
@@ -28,6 +30,8 @@ public class LitmusContext {
|
||||
Optional<String> osType;
|
||||
Optional<String> deviceId;
|
||||
Map<String, String> properties;
|
||||
Optional<String> clickStreamPayload;
|
||||
|
||||
|
||||
public Optional<String> 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<String, String> 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<String> 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<String> 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<String> 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);}
|
||||
}
|
||||
@@ -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<String, String> parameters;
|
||||
List<Constraint> constraints = Collections.emptyList();
|
||||
List<VariantDefinition> variants;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<VariantOverride> overrides;
|
||||
String stickiness;
|
||||
}
|
||||
@@ -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<String> values;
|
||||
}
|
||||
@@ -13,11 +13,37 @@
|
||||
<name>litmus-proxy</name>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.11</version>
|
||||
<scope>test</scope>
|
||||
<groupId>com.navi.medici</groupId>
|
||||
<artifactId>litmus-model</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.navi.medici</groupId>
|
||||
<artifactId>litmus-client</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.navi.medici;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*
|
||||
*/
|
||||
public class App
|
||||
{
|
||||
public static void main( String[] args )
|
||||
{
|
||||
System.out.println( "Hello World!" );
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.navi.medici.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class LitmusProxyConfig {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
20
litmus-proxy/src/main/resources/application.properties
Normal file
20
litmus-proxy/src/main/resources/application.properties
Normal file
@@ -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}
|
||||
9
litmus-proxy/src/main/resources/log4j2.properties
Normal file
9
litmus-proxy/src/main/resources/log4j2.properties
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user