variant integration with litmus

Signed-off-by: chandresh pancholi <chandresh.pancholi@navi.com>
This commit is contained in:
chandresh pancholi
2021-10-21 20:53:01 +05:30
parent c31cc33348
commit 48d8d94dbe
41 changed files with 1081 additions and 102 deletions

18
Dockerfile.core Normal file
View 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
View 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

View File

@@ -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())

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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() {

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,13 +0,0 @@
package com.navi.medici;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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));
}
}

View File

@@ -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;
}

View File

@@ -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);}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -1,13 +0,0 @@
package com.navi.medici;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,8 @@
package com.navi.medici.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LitmusProxyConfig {
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View 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}

View 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

View File

@@ -63,7 +63,13 @@
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
</dependencies>
<build>