Litmus prometheus integration

This commit is contained in:
chandresh pancholi
2022-04-11 14:52:50 +05:30
parent 062bd7244d
commit 9d81851e77
26 changed files with 350 additions and 55 deletions

View File

@@ -75,6 +75,13 @@
<artifactId>jackson-datatype-jsr310</artifactId> <artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.0</version> <version>2.13.0</version>
</dependency> </dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.8.4</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -3,7 +3,9 @@ package com.navi.medici.client;
import com.navi.medici.clickstream.ClickStreamPayload; import com.navi.medici.clickstream.ClickStreamPayload;
import com.navi.medici.config.LitmusConfig; import com.navi.medici.config.LitmusConfig;
import com.navi.medici.response.LitmusExperimentResponse;
import com.navi.medici.util.JacksonUtils; import com.navi.medici.util.JacksonUtils;
import io.micrometer.core.instrument.Counter;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@@ -24,7 +26,7 @@ public class ClickStreamClient {
this.client = new OkHttpClient(); this.client = new OkHttpClient();
} }
public <T> void publish(ClickStreamPayload<T> payload) { public <T> void publish(String experimentName, ClickStreamPayload<T> payload) {
String requestBody = JacksonUtils.objectToString(payload); String requestBody = JacksonUtils.objectToString(payload);
Request request = new Request.Builder() Request request = new Request.Builder()
.url(this.litmusConfig.getClickStreamAPI()) .url(this.litmusConfig.getClickStreamAPI())
@@ -32,6 +34,13 @@ public class ClickStreamClient {
.build(); .build();
try(Response response = client.newCall(request).execute()) { try(Response response = client.newCall(request).execute()) {
Counter.builder("litmus_client_click_stream_event_ingestion")
.tag("vertical", litmusConfig.getVertical())
.tag("status", String.valueOf(response.code()))
.tag("experiment_name", experimentName)
.tag("app_name", litmusConfig.getAppName())
.register(this.litmusConfig.getMeterRegistry())
.increment();
if (response.code() < 300) { if (response.code() < 300) {
var responseBody = response.body(); var responseBody = response.body();
assert responseBody != null; assert responseBody != null;
@@ -41,6 +50,14 @@ public class ClickStreamClient {
log.error("clickstream ingestion received non-2xx. status: {}", response.code()); log.error("clickstream ingestion received non-2xx. status: {}", response.code());
} }
} catch (Exception e) { } catch (Exception e) {
Counter.builder("litmus_client_click_stream_event_ingestion")
.tag("vertical", litmusConfig.getVertical())
.tag("experiment_name", experimentName)
.tag("app_name", litmusConfig.getAppName())
.tag("exception", e.getMessage())
.register(this.litmusConfig.getMeterRegistry())
.increment();
log.error("clickstream event ingestion failed. ", e); log.error("clickstream event ingestion failed. ", e);
} }
} }

View File

@@ -5,5 +5,5 @@ import com.navi.medici.response.LitmusExperimentResponse;
public interface ExperimentFetcher { public interface ExperimentFetcher {
LitmusExperimentResponse fetchExperiments() throws LitmusException; LitmusExperimentResponse fetchExperiments(String vertical, Long pollingTime) throws LitmusException;
} }

View File

@@ -6,6 +6,7 @@ import com.navi.medici.response.LitmusExperimentCollection;
import com.navi.medici.response.LitmusExperimentResponse; import com.navi.medici.response.LitmusExperimentResponse;
import com.navi.medici.response.LitmusResponse; import com.navi.medici.response.LitmusResponse;
import com.navi.medici.util.JacksonUtils; import com.navi.medici.util.JacksonUtils;
import io.micrometer.core.instrument.Counter;
import java.net.URL; import java.net.URL;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@@ -14,10 +15,10 @@ import okhttp3.Response;
@Log4j2 @Log4j2
public class HttpExperimentFetcher implements ExperimentFetcher { public class HttpExperimentFetcher implements ExperimentFetcher {
private final OkHttpClient client; private final OkHttpClient client;
private final URL experimentUrl; private final URL experimentUrl;
private final URL segmentIdUrl; private final URL segmentIdUrl;
private final LitmusConfig litmusConfig;
public HttpExperimentFetcher(LitmusConfig litmusConfig) { public HttpExperimentFetcher(LitmusConfig litmusConfig) {
this.client = new OkHttpClient(); this.client = new OkHttpClient();
@@ -25,29 +26,64 @@ public class HttpExperimentFetcher implements ExperimentFetcher {
litmusConfig.getLitmusURLs() litmusConfig.getLitmusURLs()
.getLitmusExperimentsURL(litmusConfig.getProjectName(), litmusConfig.getNamePrefix()); .getLitmusExperimentsURL(litmusConfig.getProjectName(), litmusConfig.getNamePrefix());
this.segmentIdUrl = litmusConfig.getLitmusURLs().getSegmentIdURL(); this.segmentIdUrl = litmusConfig.getLitmusURLs().getSegmentIdURL();
this.litmusConfig = litmusConfig;
} }
@Override @Override
public LitmusExperimentResponse fetchExperiments() throws LitmusException { public LitmusExperimentResponse fetchExperiments(String vertical, Long pollingTime) throws LitmusException {
Request request = new Request.Builder() Request request = new Request.Builder()
.url(this.experimentUrl) .url(String.format("%s?vertical=%s&polling_time=%s", this.experimentUrl, vertical, pollingTime))
.build(); .build();
try (Response response = client.newCall(request).execute()) { try (Response response = client.newCall(request).execute()) {
Counter.builder("litmus_client_fetch_experiment_polling")
.tag("vertical", vertical)
.tag("app_name", litmusConfig.getAppName())
.register(this.litmusConfig.getMeterRegistry())
.increment();
if (response.code() < 300) { if (response.code() < 300) {
var responseBody = response.body(); var responseBody = response.body();
assert responseBody != null; assert responseBody != null;
LitmusExperimentCollection experiments = JacksonUtils.stringToObject(responseBody.string(), LitmusExperimentCollection.class); LitmusExperimentCollection experiments = JacksonUtils.stringToObject(responseBody.string(), LitmusExperimentCollection.class);
Counter.builder("litmus_client_fetch_experiment_polling_total_request")
.tag("vertical", vertical)
.tag("status", String.valueOf(response.code()))
.tag("experiment_state", LitmusExperimentResponse.Status.CHANGED.name())
.tag("app_name", litmusConfig.getAppName())
.register(this.litmusConfig.getMeterRegistry())
.increment();
return new LitmusExperimentResponse(LitmusExperimentResponse.Status.CHANGED, experiments); return new LitmusExperimentResponse(LitmusExperimentResponse.Status.CHANGED, experiments);
} else if(response.code() == 304) { } else if (response.code() == 304) {
Counter.builder("litmus_client_fetch_experiment_polling_request_status")
.tag("vertical", vertical)
.tag("app_name", litmusConfig.getAppName())
.tag("status", String.valueOf(response.code()))
.tag("experiment_state", LitmusExperimentResponse.Status.NOT_CHANGED.name())
.register(this.litmusConfig.getMeterRegistry())
.increment();
return new LitmusExperimentResponse(LitmusExperimentResponse.Status.NOT_CHANGED, response.code()); return new LitmusExperimentResponse(LitmusExperimentResponse.Status.NOT_CHANGED, response.code());
} else { } else {
Counter.builder("litmus_client_fetch_experiment_polling_request_status")
.tag("vertical", vertical)
.tag("app_name", litmusConfig.getAppName())
.tag("status", String.valueOf(response.code()))
.tag("experiment_state", LitmusExperimentResponse.Status.NOT_CHANGED.name())
.register(this.litmusConfig.getMeterRegistry())
.increment();
return new LitmusExperimentResponse(LitmusExperimentResponse.Status.UNAVAILABLE, response.code()); return new LitmusExperimentResponse(LitmusExperimentResponse.Status.UNAVAILABLE, response.code());
} }
} catch (Exception e) { } catch (Exception e) {
Counter.builder("litmus_client_fetch_experiment_polling_request_failed")
.tag("vertical", vertical)
.tag("app_name", litmusConfig.getAppName())
.register(this.litmusConfig.getMeterRegistry())
.increment();
throw new LitmusException("fetch experiments failed.", e); throw new LitmusException("fetch experiments failed.", e);
} }
} }
@@ -57,12 +93,26 @@ public class HttpExperimentFetcher implements ExperimentFetcher {
.url(String.format("%s?segment_name=%s&id=%s", this.segmentIdUrl, segmentName, id)) .url(String.format("%s?segment_name=%s&id=%s", this.segmentIdUrl, segmentName, id))
.build(); .build();
try(Response response = client.newCall(request).execute()) { try (Response response = client.newCall(request).execute()) {
var responseBody = response.body(); var responseBody = response.body();
assert responseBody != null; assert responseBody != null;
Counter.builder("litmus_client_segment_id_exist_request")
.tag("vertical", litmusConfig.getVertical())
.tag("app_name", litmusConfig.getAppName())
.tag("status", String.valueOf(response.code()))
.register(this.litmusConfig.getMeterRegistry())
.increment();
return JacksonUtils.stringToObject(responseBody.string(), LitmusResponse.class); return JacksonUtils.stringToObject(responseBody.string(), LitmusResponse.class);
} catch (Exception e) { } catch (Exception e) {
Counter.builder("litmus_client_segment_id_exist_request_failed")
.tag("vertical", litmusConfig.getVertical())
.tag("segment_name", segmentName)
.tag("app_name", litmusConfig.getAppName())
.tag("exception", e.getMessage())
.register(this.litmusConfig.getMeterRegistry())
.increment();
throw new LitmusException("segment name to id matching exists.", e); throw new LitmusException("segment name to id matching exists.", e);
} }
} }

View File

@@ -8,6 +8,7 @@ import com.navi.medici.scheduler.LitmusScheduledExecutorImpl;
import com.navi.medici.strategy.Strategy; import com.navi.medici.strategy.Strategy;
import com.navi.medici.strategy.UnknownStrategy; import com.navi.medici.strategy.UnknownStrategy;
import com.navi.medici.util.LitmusURLs; import com.navi.medici.util.LitmusURLs;
import io.micrometer.core.instrument.MeterRegistry;
import java.io.File; import java.io.File;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.URI; import java.net.URI;
@@ -37,6 +38,8 @@ public class LitmusConfig {
private final Strategy fallbackStrategy; private final Strategy fallbackStrategy;
private final LitmusExperimentBootstrapProvider litmusExperimentBootstrapProvider; private final LitmusExperimentBootstrapProvider litmusExperimentBootstrapProvider;
private final String clickStreamAPI; private final String clickStreamAPI;
private final String vertical;
private final MeterRegistry meterRegistry;
private LitmusConfig( private LitmusConfig(
@@ -52,7 +55,9 @@ public class LitmusConfig {
LitmusScheduledExecutor litmusScheduledExecutor, LitmusScheduledExecutor litmusScheduledExecutor,
Strategy fallbackStrategy, Strategy fallbackStrategy,
LitmusExperimentBootstrapProvider litmusBootstrapProvider, LitmusExperimentBootstrapProvider litmusBootstrapProvider,
String clickStreamAPI) { String clickStreamAPI,
String vertical,
MeterRegistry meterRegistry) {
if (appName == null) { if (appName == null) {
throw new IllegalStateException("You are required to specify the litmus appName"); throw new IllegalStateException("You are required to specify the litmus appName");
@@ -70,6 +75,19 @@ public class LitmusConfig {
throw new IllegalStateException("You are required to specify a scheduler"); throw new IllegalStateException("You are required to specify a scheduler");
} }
if (clickStreamAPI == null) {
throw new IllegalStateException("You are required to specify a scheduler");
}
if (vertical == null) {
throw new IllegalStateException("You are required to specify vertical");
}
if (meterRegistry == null) {
throw new IllegalStateException("You are required to specify meter registry");
}
this.fallbackStrategy = Objects.requireNonNullElseGet(fallbackStrategy, UnknownStrategy::new); this.fallbackStrategy = Objects.requireNonNullElseGet(fallbackStrategy, UnknownStrategy::new);
this.litmusAPI = litmusAPI; this.litmusAPI = litmusAPI;
@@ -85,6 +103,8 @@ public class LitmusConfig {
this.litmusScheduledExecutor = litmusScheduledExecutor; this.litmusScheduledExecutor = litmusScheduledExecutor;
this.litmusExperimentBootstrapProvider = litmusBootstrapProvider; this.litmusExperimentBootstrapProvider = litmusBootstrapProvider;
this.clickStreamAPI = clickStreamAPI; this.clickStreamAPI = clickStreamAPI;
this.vertical = vertical;
this.meterRegistry = meterRegistry;
} }
public static Builder builder() { public static Builder builder() {
@@ -150,6 +170,12 @@ public class LitmusConfig {
return clickStreamAPI; return clickStreamAPI;
} }
public String getVertical() {return vertical;}
public MeterRegistry getMeterRegistry() {
return meterRegistry;
}
public static class Builder { public static class Builder {
private URI litmusAPI; private URI litmusAPI;
@@ -165,6 +191,8 @@ public class LitmusConfig {
private @Nullable Strategy fallbackStrategy; private @Nullable Strategy fallbackStrategy;
private @Nullable LitmusExperimentBootstrapProvider litmusExperimentBootstrapProvider; private @Nullable LitmusExperimentBootstrapProvider litmusExperimentBootstrapProvider;
private @Nullable String clickStreamAPI; private @Nullable String clickStreamAPI;
private String vertical;
private MeterRegistry meterRegistry;
private static String getHostname() { private static String getHostname() {
String hostName = System.getProperty("hostname"); String hostName = System.getProperty("hostname");
@@ -252,6 +280,17 @@ public class LitmusConfig {
return this; return this;
} }
public Builder vertical(String vertical) {
this.vertical = vertical;
return this;
}
public Builder meterRegistry(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
return this;
}
private String getBackupFile() { private String getBackupFile() {
if (backupFile != null) { if (backupFile != null) {
return backupFile; return backupFile;
@@ -277,7 +316,9 @@ public class LitmusConfig {
.orElseGet(LitmusScheduledExecutorImpl::getInstance), .orElseGet(LitmusScheduledExecutorImpl::getInstance),
fallbackStrategy, fallbackStrategy,
litmusExperimentBootstrapProvider, litmusExperimentBootstrapProvider,
clickStreamAPI); clickStreamAPI,
vertical,
meterRegistry);
} }
public String getDefaultSdkVersion() { public String getDefaultSdkVersion() {

View File

@@ -11,6 +11,7 @@ import com.navi.medici.request.v1.LitmusExperiment;
import com.navi.medici.util.JacksonUtils; import com.navi.medici.util.JacksonUtils;
import com.navi.medici.variants.Variant; import com.navi.medici.variants.Variant;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
public class EventDispatcher { public class EventDispatcher {
private final ClickStreamClient clickStreamClient; private final ClickStreamClient clickStreamClient;
@@ -47,11 +48,11 @@ public class EventDispatcher {
.customerReferenceId(litmusContext.getUserId().orElse("")) .customerReferenceId(litmusContext.getUserId().orElse(""))
.build()); .build());
clickStreamClient.publish(clickStreamPayload); CompletableFuture.supplyAsync(() -> {
clickStreamClient.publish(litmusExperiment.getName(), clickStreamPayload);
return null;
});
} }
} }
private void clickstreamIngestion() {
}
} }

View File

@@ -23,6 +23,8 @@ import com.navi.medici.strategy.UserWithIdStrategy;
import com.navi.medici.util.JacksonUtils; import com.navi.medici.util.JacksonUtils;
import com.navi.medici.util.VariantUtil; import com.navi.medici.util.VariantUtil;
import com.navi.medici.variants.Variant; import com.navi.medici.variants.Variant;
import io.micrometer.core.instrument.Counter;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -33,9 +35,12 @@ import lombok.extern.log4j.Log4j2;
@Log4j2 @Log4j2
public class DefaultLitmus implements Litmus { public class DefaultLitmus implements Litmus {
public static final UnknownStrategy UNKNOWN_STRATEGY = new UnknownStrategy(); public static final UnknownStrategy UNKNOWN_STRATEGY = new UnknownStrategy();
public static final Variant DISABLED_VARIANT = new Variant("disabled", null, false, null); public static final Variant DISABLED_VARIANT = new Variant("disabled", null, false, null);
private final ExperimentRepository experimentRepository; private final ExperimentRepository experimentRepository;
private final Map<String, Strategy> strategyMap; private final Map<String, Strategy> strategyMap;
private final LitmusContextProvider contextProvider; private final LitmusContextProvider contextProvider;
@@ -149,6 +154,12 @@ public class DefaultLitmus implements Litmus {
}); });
} }
Counter.builder("litmus_client_experiment_check_enabled")
.tag("experiment_name", experimentName)
.tag("result", String.valueOf(enabled))
.register(this.config.getMeterRegistry())
.increment();
this.eventDispatcher.publish(context, litmusExperiment, enabled, null); this.eventDispatcher.publish(context, litmusExperiment, enabled, null);
log.info("experiment_name: {}, result: {}", experimentName, enabled); log.info("experiment_name: {}, result: {}", experimentName, enabled);
return enabled; return enabled;
@@ -204,6 +215,14 @@ public class DefaultLitmus implements Litmus {
this.eventDispatcher.publish(context, litmusExperiment, enabled, variant); this.eventDispatcher.publish(context, litmusExperiment, enabled, variant);
log.info("experiment_name: {}, result: {}", experimentName, enabled); log.info("experiment_name: {}, result: {}", experimentName, enabled);
Counter.builder("litmus_client_experiment_variant")
.tag("experiment_name", experimentName)
.tag("result", String.valueOf(enabled))
.tag("variant", variant.getName())
.register(this.config.getMeterRegistry())
.increment();
return variant; return variant;
} }
@@ -217,4 +236,7 @@ public class DefaultLitmus implements Litmus {
return getVariant(experimentName, contextProvider.getContext(), defaultValue); return getVariant(experimentName, contextProvider.getContext(), defaultValue);
} }
} }

View File

@@ -51,18 +51,18 @@ public class LitmusExperimentRepository implements ExperimentRepository {
} }
if (litmusConfig.isSynchronousFetchOnInitialisation()) { if (litmusConfig.isSynchronousFetchOnInitialisation()) {
updateExperiments().run(); updateExperiments(litmusConfig.getVertical(), litmusConfig.getFetchLitmusExperimentsInterval()).run();
} }
this.inMemoryCache = new InMemoryCache(this.experimentCollection); this.inMemoryCache = new InMemoryCache(this.experimentCollection);
executor.setInterval(updateExperiments(), 0, litmusConfig.getFetchLitmusExperimentsInterval()); executor.setInterval(updateExperiments(litmusConfig.getVertical(), litmusConfig.getFetchLitmusExperimentsInterval()), 0, litmusConfig.getFetchLitmusExperimentsInterval());
} }
private Runnable updateExperiments() { private Runnable updateExperiments(String vertical, Long pollingTime) {
return () -> { return () -> {
try { try {
LitmusExperimentResponse response = experimentFetcher.fetchExperiments(); LitmusExperimentResponse response = experimentFetcher.fetchExperiments(vertical, pollingTime);
if (response.getStatus() == LitmusExperimentResponse.Status.CHANGED) { if (response != null && LitmusExperimentResponse.Status.CHANGED == response.getStatus()) {
experimentCollection = response.getExperimentCollection(); experimentCollection = response.getExperimentCollection();
experimentBackupHandler.write(response.getExperimentCollection()); experimentBackupHandler.write(response.getExperimentCollection());
this.inMemoryCache = new InMemoryCache(experimentCollection); this.inMemoryCache = new InMemoryCache(experimentCollection);

View File

@@ -1,9 +1,15 @@
package com.navi.medici.strategy; package com.navi.medici.strategy;
import static com.navi.medici.strategy.FlexibleRolloutStrategy.GROUP_ID;
import static com.navi.medici.strategy.FlexibleRolloutStrategy.PERCENTAGE;
import com.navi.medici.client.HttpExperimentFetcher; import com.navi.medici.client.HttpExperimentFetcher;
import com.navi.medici.context.LitmusContext; import com.navi.medici.context.LitmusContext;
import com.navi.medici.util.StrategyUtils;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
public class DeviceWithIdStrategy implements Strategy { public class DeviceWithIdStrategy implements Strategy {
@@ -28,10 +34,30 @@ public class DeviceWithIdStrategy implements Strategy {
@Override @Override
public boolean isEnabled(Map<String, String> parameters, LitmusContext litmusContext) { public boolean isEnabled(Map<String, String> parameters, LitmusContext litmusContext) {
if (StringUtils.isBlank(parameters.get(PERCENTAGE))) {
return deviceWithIdSegmentCheck(parameters, litmusContext);
} else {
return segmentCheckWithRollout(parameters, litmusContext);
}
}
private boolean deviceWithIdSegmentCheck(Map<String, String> parameters, LitmusContext litmusContext) {
var deviceId = litmusContext.getDeviceId().orElse(null);
var segmentName = parameters.get(SEGMENT); var segmentName = parameters.get(SEGMENT);
var result = experimentFetcher.segmentIdExists(segmentName, litmusContext.getDeviceId().orElse(null)); var result = experimentFetcher.segmentIdExists(segmentName, deviceId);
return result.getData() != null && (Boolean) result.getData(); return result.getData() != null && (Boolean) result.getData();
}
private boolean segmentCheckWithRollout(Map<String, String> parameters, LitmusContext litmusContext) {
var deviceId = litmusContext.getDeviceId().orElse(null);
var percentage = StrategyUtils.getPercentage(parameters.get(PERCENTAGE));
var groupId = parameters.getOrDefault(GROUP_ID, "");
var norm = StrategyUtils.getNormalizedNumber(deviceId, groupId);
return Optional.of(deviceWithIdSegmentCheck(parameters, litmusContext))
.map(r -> r && percentage > 0 && norm <= percentage)
.orElse(false);
} }
} }

View File

@@ -15,13 +15,6 @@ public class FlexibleRolloutStrategy implements Strategy {
protected static final String PERCENTAGE = "rollout"; protected static final String PERCENTAGE = "rollout";
protected static final String GROUP_ID = "groupId"; protected static final String GROUP_ID = "groupId";
private final Supplier<String> randomGenerator;
public FlexibleRolloutStrategy() {
this.randomGenerator = () -> Math.random() * 100 + "";
}
@Override @Override
public String getName() { public String getName() {
return "flexibleRollout"; return "flexibleRollout";
@@ -32,32 +25,12 @@ public class FlexibleRolloutStrategy implements Strategy {
return false; return false;
} }
private Optional<String> resolveStickiness(String stickiness, LitmusContext context) {
switch (stickiness) {
case "userId":
return context.getUserId();
case "sessionId":
return context.getSessionId();
case "deviceId":
return context.getDeviceId();
case "appVersionCode":
return context.getAppVersionCode();
case "osType":
return context.getOsType();
case "random":
return Optional.of(randomGenerator.get());
case "default":
return Optional.of(context.getUserId()
.orElse(context.getSessionId().orElse(this.randomGenerator.get())));
default:
return context.getByName(stickiness);
}
}
@Override @Override
public boolean isEnabled(Map<String, String> parameters, LitmusContext litmusContext) { public boolean isEnabled(Map<String, String> parameters, LitmusContext litmusContext) {
final String stickiness = getStickiness(parameters); final String stickiness = getStickiness(parameters);
final Optional<String> stickinessId = resolveStickiness(stickiness, litmusContext); Optional<String> stickinessId = StrategyUtils.resolveStickiness(stickiness, litmusContext);
final int percentage = StrategyUtils.getPercentage(parameters.get(PERCENTAGE)); final int percentage = StrategyUtils.getPercentage(parameters.get(PERCENTAGE));
final String groupId = parameters.getOrDefault(GROUP_ID, ""); final String groupId = parameters.getOrDefault(GROUP_ID, "");

View File

@@ -1,9 +1,15 @@
package com.navi.medici.strategy; package com.navi.medici.strategy;
import static com.navi.medici.strategy.FlexibleRolloutStrategy.GROUP_ID;
import static com.navi.medici.strategy.FlexibleRolloutStrategy.PERCENTAGE;
import com.navi.medici.client.HttpExperimentFetcher; import com.navi.medici.client.HttpExperimentFetcher;
import com.navi.medici.context.LitmusContext; import com.navi.medici.context.LitmusContext;
import com.navi.medici.util.StrategyUtils;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
public class UserWithIdStrategy implements Strategy { public class UserWithIdStrategy implements Strategy {
@@ -28,10 +34,31 @@ public class UserWithIdStrategy implements Strategy {
@Override @Override
public boolean isEnabled(Map<String, String> parameters, LitmusContext litmusContext) { public boolean isEnabled(Map<String, String> parameters, LitmusContext litmusContext) {
if (StringUtils.isBlank(parameters.get(PERCENTAGE))) {
return userWithIdSegmentCheck(parameters, litmusContext);
} else {
return segmentCheckWithRollout(parameters, litmusContext);
}
}
private boolean userWithIdSegmentCheck(Map<String, String> parameters, LitmusContext litmusContext) {
var userId = litmusContext.getUserId().orElse(null);
var segmentName = parameters.get(SEGMENT); var segmentName = parameters.get(SEGMENT);
var result = experimentFetcher.segmentIdExists(segmentName, litmusContext.getUserId().orElse(null)); var result = experimentFetcher.segmentIdExists(segmentName, userId);
return result.getData() != null && (Boolean) result.getData(); return result.getData() != null && (Boolean) result.getData();
} }
private boolean segmentCheckWithRollout(Map<String, String> parameters, LitmusContext litmusContext) {
var userId = litmusContext.getUserId().orElse(null);
var percentage = StrategyUtils.getPercentage(parameters.get(PERCENTAGE));
var groupId = parameters.getOrDefault(GROUP_ID, "");
var norm = StrategyUtils.getNormalizedNumber(userId, groupId);
return Optional.of(userWithIdSegmentCheck(parameters, litmusContext))
.map(r -> r && percentage > 0 && norm <= percentage)
.orElse(false);
}
} }

View File

@@ -1,7 +1,10 @@
package com.navi.medici.util; package com.navi.medici.util;
import com.navi.medici.annotation.Nullable; import com.navi.medici.annotation.Nullable;
import com.navi.medici.context.LitmusContext;
import com.sangupta.murmur.Murmur3; import com.sangupta.murmur.Murmur3;
import java.util.Optional;
import java.util.function.Supplier;
public class StrategyUtils { public class StrategyUtils {
@@ -47,4 +50,27 @@ public class StrategyUtils {
} }
} }
public static Optional<String> resolveStickiness(String stickiness, LitmusContext context) {
Supplier<String> randomGenerator = () -> Math.random() * 100 + "";
switch (stickiness) {
case "userId":
return context.getUserId();
case "sessionId":
return context.getSessionId();
case "deviceId":
return context.getDeviceId();
case "appVersionCode":
return context.getAppVersionCode();
case "osType":
return context.getOsType();
case "random":
return Optional.of(randomGenerator.get());
case "default":
return Optional.of(context.getUserId()
.orElse(context.getSessionId().orElse(randomGenerator.get())));
default:
return context.getByName(stickiness);
}
}
} }

View File

@@ -8,13 +8,16 @@ import io.micrometer.core.annotation.Timed;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@@ -37,6 +40,19 @@ public class ExperimentController {
return experimentService.fetchAllExperiments(); return experimentService.fetchAllExperiments();
} }
@GetMapping(value = "/vertical")
@Timed(value = "litmus.fetch.vertical.experiment", percentiles = {0.95, 0.99})
public ResponseEntity<LitmusExperimentCollection> fetchExperimentsForAVertical(@RequestParam("vertical") String vertical,
@RequestParam("polling_time") Long pollingTime) {
var collection = experimentService.fetchAllExperimentsForVerticals(vertical, pollingTime);
if (collection.getLitmusExperiments().isEmpty()) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).body(collection);
}
return ResponseEntity.ok(collection);
}
@PutMapping(value = "/attach/variants/{experiment_id}", consumes = MediaType.APPLICATION_JSON_VALUE) @PutMapping(value = "/attach/variants/{experiment_id}", consumes = MediaType.APPLICATION_JSON_VALUE)
@Timed(value = "litmus.attach.variants", percentiles = {0.95, 0.99}) @Timed(value = "litmus.attach.variants", percentiles = {0.95, 0.99})
public void attachVariants(@PathVariable("experiment_id") String experimentId, public void attachVariants(@PathVariable("experiment_id") String experimentId,

View File

@@ -6,9 +6,12 @@ import com.navi.medici.variants.VariantDefinition;
import java.util.List; import java.util.List;
public interface ExperimentService { public interface ExperimentService {
void create(LitmusExperiment litmusExperimentRequest); void create(LitmusExperiment litmusExperimentRequest);
LitmusExperimentCollection fetchAllExperiments(); LitmusExperimentCollection fetchAllExperiments();
void attachVariants(String experimentId, List<VariantDefinition> variantDefinitions); void attachVariants(String experimentId, List<VariantDefinition> variantDefinitions);
LitmusExperimentCollection fetchAllExperimentsForVerticals(String vertical, Long pollingTime);
} }

View File

@@ -1,5 +1,8 @@
package com.navi.medici.service.experiment; package com.navi.medici.service.experiment;
import static com.navi.medici.enums.ExperimentType.KILL_SWITCH;
import static com.navi.medici.enums.ExperimentType.RELEASE;
import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentEntity;
import com.navi.medici.exceptions.BadRequestException; import com.navi.medici.exceptions.BadRequestException;
import com.navi.medici.query.experiment.IExperimentQuery; import com.navi.medici.query.experiment.IExperimentQuery;
@@ -41,9 +44,16 @@ public record ExperimentServiceImpl(IExperimentQuery experimentQuery,
@Override @Override
public LitmusExperimentCollection fetchAllExperiments() { public LitmusExperimentCollection fetchAllExperiments() {
List<ExperimentEntity> experimentEntities = experimentQuery.findByEnabled(true); List<ExperimentEntity> experimentEntities = experimentQuery.findByEnabled(true);
List<LitmusExperiment> litmusExperiments = experimentEntities.stream() List<LitmusExperiment> litmusExperiments = experimentEntities.stream()
.map(this::build) .map(this::build)
.collect(Collectors.toList()); .peek(le -> {
//This is to reduce sending strategy string over network
if (RELEASE.equals(le.getType()) || KILL_SWITCH.equals(le.getType())) {
le.setStrategies(null);
}
}).collect(Collectors.toList());
return LitmusExperimentCollection.builder() return LitmusExperimentCollection.builder()
.litmusExperiments(litmusExperiments) .litmusExperiments(litmusExperiments)
.build(); .build();
@@ -63,6 +73,19 @@ public record ExperimentServiceImpl(IExperimentQuery experimentQuery,
experimentQuery.save(experiment); experimentQuery.save(experiment);
} }
@Override
public LitmusExperimentCollection fetchAllExperimentsForVerticals(String vertical, Long pollingTime) {
var experimentEntities = experimentQuery.findByVertical(vertical, pollingTime);
List<LitmusExperiment> litmusExperiments = experimentEntities.stream()
.map(this::build)
.collect(Collectors.toList());
return LitmusExperimentCollection.builder()
.litmusExperiments(litmusExperiments)
.build();
}
private LitmusExperiment build(ExperimentEntity experimentEntity) { private LitmusExperiment build(ExperimentEntity experimentEntity) {
return LitmusExperiment.builder() return LitmusExperiment.builder()
.experimentId(experimentEntity.getExperimentId()) .experimentId(experimentEntity.getExperimentId())
@@ -75,6 +98,7 @@ public record ExperimentServiceImpl(IExperimentQuery experimentQuery,
.type(experimentEntity.getType()) .type(experimentEntity.getType())
.startTime(experimentEntity.getStartTime()) .startTime(experimentEntity.getStartTime())
.endTime(experimentEntity.getEndTime()) .endTime(experimentEntity.getEndTime())
.vertical(experimentEntity.getVertical())
.build(); .build();
} }
} }

View File

@@ -14,7 +14,7 @@ spring.jpa.hibernate.ddl-auto=none
#spring.jpa.properties.hibernate.use_sql_comments=true #spring.jpa.properties.hibernate.use_sql_comments=true
#spring.jpa.properties.hibernate.format_sql=true #spring.jpa.properties.hibernate.format_sql=true
management.server.port=4001 management.server.port=4000
management.endpoints.web.exposure.include=prometheus,health,info,metric,heapdump,threaddump management.endpoints.web.exposure.include=prometheus,health,info,metric,heapdump,threaddump
server.tomcat.mbeanregistry.enabled=true server.tomcat.mbeanregistry.enabled=true
spring.jmx.enabled=true spring.jmx.enabled=true

View File

@@ -65,6 +65,9 @@ public class ExperimentEntity {
@Column(name = "end_time") @Column(name = "end_time")
LocalDateTime endTime; LocalDateTime endTime;
@Column(name = "vertical")
String vertical;
@Version @Version
private Integer version; private Integer version;

View File

@@ -2,6 +2,7 @@ package com.navi.medici.query.experiment;
import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentEntity;
import com.navi.medici.repository.ExperimentRepository; import com.navi.medici.repository.ExperimentRepository;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -10,6 +11,7 @@ import org.springframework.stereotype.Component;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class ExperimentQueryImpl implements IExperimentQuery { public class ExperimentQueryImpl implements IExperimentQuery {
private final ExperimentRepository experimentRepository; private final ExperimentRepository experimentRepository;
@Override @Override
@@ -27,6 +29,13 @@ public class ExperimentQueryImpl implements IExperimentQuery {
return experimentRepository.findByExperimentId(experimentId); return experimentRepository.findByExperimentId(experimentId);
} }
@Override
public List<ExperimentEntity> findByVertical(String vertical, Long pollingTime) {
var lastPollingTime = LocalDateTime.now().minusSeconds(pollingTime);
return experimentRepository.findByVerticalAndUpdatedTime(vertical, lastPollingTime, LocalDateTime.now());
}
@Override @Override
public void save(ExperimentEntity experiment) { public void save(ExperimentEntity experiment) {
experimentRepository.save(experiment); experimentRepository.save(experiment);

View File

@@ -11,5 +11,7 @@ public interface IExperimentQuery {
Optional<ExperimentEntity> findByExperimentId(String experimentId); Optional<ExperimentEntity> findByExperimentId(String experimentId);
List<ExperimentEntity> findByVertical(String vertical, Long pollingTime);
void save(ExperimentEntity experiment); void save(ExperimentEntity experiment);
} }

View File

@@ -1,16 +1,26 @@
package com.navi.medici.repository; package com.navi.medici.repository;
import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentEntity;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@Repository @Repository
public interface ExperimentRepository extends CrudRepository<ExperimentEntity, Long> { public interface ExperimentRepository extends CrudRepository<ExperimentEntity, Long> {
List<ExperimentEntity> findByEnabled(Boolean enabled); List<ExperimentEntity> findByEnabled(Boolean enabled);
Optional<ExperimentEntity> findByName(String name); Optional<ExperimentEntity> findByName(String name);
Optional<ExperimentEntity> findByExperimentId(String experimentId); Optional<ExperimentEntity> findByExperimentId(String experimentId);
@Query(value = "select * from experiments e where e.vertical = :vertical and e.updated_at >= :lastPollingTime and e.updated_at <= :currentTime ",
nativeQuery = true)
List<ExperimentEntity> findByVerticalAndUpdatedTime(@Param("vertical") String vertical,
@Param("lastPollingTime") LocalDateTime lastPollingTime,
@Param("currentTime") LocalDateTime currentTime);
} }

View File

@@ -0,0 +1,6 @@
--liquibase formatted sql
--changeset author:chandresh id:202204011642
ALTER TABLE experiments
ADD COLUMN vertical VARCHAR;

View File

@@ -27,7 +27,18 @@
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId> <artifactId>spring-boot-starter-actuator</artifactId>
<exclusions>
<exclusion>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -36,6 +47,18 @@
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.8.4</version>
</dependency>
<dependency>
<groupId>bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>140</version>
</dependency>
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId> <artifactId>javax.servlet-api</artifactId>

View File

@@ -3,6 +3,7 @@ package com.navi.medici.container;
import com.navi.medici.config.LitmusConfig; import com.navi.medici.config.LitmusConfig;
import com.navi.medici.litmus.DefaultLitmus; import com.navi.medici.litmus.DefaultLitmus;
import com.navi.medici.litmus.Litmus; import com.navi.medici.litmus.Litmus;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -10,13 +11,15 @@ import org.springframework.stereotype.Component;
public class MockContainer { public class MockContainer {
@Bean @Bean
public Litmus litmus() { public Litmus litmus(MeterRegistry meterRegistry) {
var litmusConfig = LitmusConfig.builder() var litmusConfig = LitmusConfig.builder()
.litmusAPI("http://localhost:12000/litmus-core/v1") .litmusAPI("http://localhost:12000/litmus-core/v1")
.appName("litmus-mock") .appName("litmus-mock")
.instanceId("test-instance") .instanceId("test-instance")
.litmusContextProvider(new CustomLitmusContextProvider()) .litmusContextProvider(new CustomLitmusContextProvider())
.clickStreamAPI("https://dev-janus.np.navi-tech.in/events/json") .clickStreamAPI("https://dev-janus.np.navi-tech.in/events/json")
.vertical("PL")
.meterRegistry(meterRegistry)
.build(); .build();
Litmus litmus = new DefaultLitmus(litmusConfig); Litmus litmus = new DefaultLitmus(litmusConfig);

View File

@@ -1 +1,4 @@
server.port=11000 server.port=11000
management.server.port=4001
management.endpoints.web.exposure.include=prometheus,health,info,metric,heapdump,threaddump

View File

@@ -31,6 +31,8 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId> <artifactId>spring-boot-starter-actuator</artifactId>

View File

@@ -20,6 +20,7 @@ public record LitmusProxyContainer(LitmusProxyConfig litmusProxyConfig, LitmusCo
.instanceId("test-instance") .instanceId("test-instance")
.litmusContextProvider(litmusContextProvider) .litmusContextProvider(litmusContextProvider)
.clickStreamAPI(litmusProxyConfig.getClickStreamEndpoint()) .clickStreamAPI(litmusProxyConfig.getClickStreamEndpoint())
.vertical("PL")
.build(); .build();
Litmus litmus = new DefaultLitmus(litmusConfig); Litmus litmus = new DefaultLitmus(litmusConfig);