diff --git a/proctor-consumer/src/main/java/com/indeed/proctor/consumer/AbstractGroups.java b/proctor-consumer/src/main/java/com/indeed/proctor/consumer/AbstractGroups.java index 68f3be266..28b73b359 100644 --- a/proctor-consumer/src/main/java/com/indeed/proctor/consumer/AbstractGroups.java +++ b/proctor-consumer/src/main/java/com/indeed/proctor/consumer/AbstractGroups.java @@ -355,6 +355,28 @@ public String toLoggingString() { return sb.toString(); } + /** + * String to be used for verification/debugging purposes. For historic reasons inside Indeed, + * contains two output formats per testname. This method does not filter out 100% allocations. + * + *

Additional custom groups can be added by overriding getCustomGroupsForLogging(). + * + * @return a comma-separated List of {testname}{active-bucket-VALUE} and + * {AllocationId}{testname}{active-bucket-VALUE} for all LIVE tests + */ + public String toVerifyGroupsString() { + if (isEmpty()) { + return ""; + } + final StringBuilder sb = new StringBuilder(proctorResult.getBuckets().size() * 10); + appendTestGroups(sb, GROUPS_SEPARATOR, false); + // remove trailing comma + if (sb.length() > 0) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + /** * @return an empty string or a comma-separated, comma-finalized list of groups * @deprecated use toLoggingString() @@ -398,12 +420,61 @@ public void appendTestGroups(final StringBuilder sb, final char separator) { appendTestGroupsWithAllocations(sb, separator, testNames); } + /** + * For each testname returned by getLoggingTestNames(), appends each test group in the form of + * [allocation id + ":" + test name + bucket value]. For example, it appends "#A1:bgcolortst1" + * if test name is bgcolortst and allocation id is #A1 and separator is ",". + * + *

the separator should be appended after each test group added to the string builder {@link + * #toString()} {@link #buildTestGroupString()} or {@link #appendTestGroups(StringBuilder)} + * + * @param sb a string builder + * @param separator a char used as separator x appends an empty string or a x-separated, + * x-finalized list of groups + */ + public void appendTestGroups( + final StringBuilder sb, + final char separator, + final boolean filterRolledOutAllocations) { + final List testNames = + filterRolledOutAllocations ? getLoggingTestNames() : getLoggingTestNamesFullList(); + appendTestGroupsWithAllocations(sb, separator, testNames); + } + + /** + * Return test names for tests that are non-silent, doesn't have available definition, is not + * 100% rolled out, and have a non-negative active bucket, in a stable sort. Stable sort is + * beneficial for log string compression, for debugging, and may help in cases of size-limited + * output. + */ + protected final List getLoggingTestNames() { + final Map testDefinitions = + proctorResult.getTestDefinitions(); + // following lines should preserve the order in the map to ensure logging values are stable + final Map buckets = proctorResult.getBuckets(); + return buckets.keySet().stream() + .filter( + testName -> { + final ConsumableTestDefinition consumableTestDefinition = + testDefinitions.get(testName); + // fallback to non-silent when test definition is not available + return (consumableTestDefinition == null) + || !consumableTestDefinition.getSilent(); + }) + // Suppress 100% allocation logging + .filter(this::loggableAllocation) + // call to getValueWithouMarkingUsage() to allow overrides of getActiveBucket, but + // avoid marking + .filter(testName -> getValueWithoutMarkingUsage(testName, -1) >= 0) + .collect(Collectors.toList()); + } + /** * Return test names for tests that are non-silent or doesn't have available definition, and * have a non-negative active bucket, in a stable sort. Stable sort is beneficial for log string * compression, for debugging, and may help in cases of size-limited output. */ - protected final List getLoggingTestNames() { + protected final List getLoggingTestNamesFullList() { final Map testDefinitions = proctorResult.getTestDefinitions(); // following lines should preserve the order in the map to ensure logging values are stable @@ -454,6 +525,25 @@ protected final void appendTestGroupsWithAllocations( } } + private boolean loggableAllocation(final String testName) { + final ConsumableTestDefinition td = proctorResult.getTestDefinitions().get(testName); + return loggableAllocation(testName, td, proctorResult); + } + + public static boolean loggableAllocation( + final String testName, + final ConsumableTestDefinition td, + final ProctorResult proctorResult) { + if (td != null) { + final Allocation allocation = proctorResult.getAllocations().get(testName); + if (allocation != null + && allocation.getRanges().stream().anyMatch(range -> range.getLength() == 1)) { + return td.getForceLogging(); + } + } + return true; + } + /** * Generates a Map[testname, bucketValue] for bucketValues >= 0. * diff --git a/proctor-consumer/src/main/java/com/indeed/proctor/consumer/ProctorGroupsWriter.java b/proctor-consumer/src/main/java/com/indeed/proctor/consumer/ProctorGroupsWriter.java index af97f5011..6380f74ba 100644 --- a/proctor-consumer/src/main/java/com/indeed/proctor/consumer/ProctorGroupsWriter.java +++ b/proctor-consumer/src/main/java/com/indeed/proctor/consumer/ProctorGroupsWriter.java @@ -14,6 +14,8 @@ import java.util.Optional; import java.util.function.BiPredicate; +import static com.indeed.proctor.consumer.AbstractGroups.loggableAllocation; + /** * Helper class to build a Strings to log, helping with analysis of experiments. * @@ -203,7 +205,9 @@ public ProctorGroupsWriter build() { if (additionalFilter != null) { return additionalFilter.test(testName, proctorResult); } - return true; + // Suppress 100% allocation logging + return loggableAllocation( + testName, consumableTestDefinition, proctorResult); }); } } diff --git a/proctor-consumer/src/main/java/com/indeed/proctor/consumer/spring/ShowGroupsHandler.java b/proctor-consumer/src/main/java/com/indeed/proctor/consumer/spring/ShowGroupsHandler.java index 684db45e0..0525eae1f 100644 --- a/proctor-consumer/src/main/java/com/indeed/proctor/consumer/spring/ShowGroupsHandler.java +++ b/proctor-consumer/src/main/java/com/indeed/proctor/consumer/spring/ShowGroupsHandler.java @@ -29,7 +29,7 @@ public void handleRequest(HttpServletRequest request, HttpServletResponse respon writer.println("Did not determine any groups"); } else { final StringBuilder sb = new StringBuilder(); - grps.appendTestGroups(sb, '\n'); + grps.appendTestGroups(sb, '\n', false); writer.print(sb.toString()); } } diff --git a/proctor-consumer/src/test/java/com/indeed/proctor/consumer/ProctorGroupStubber.java b/proctor-consumer/src/test/java/com/indeed/proctor/consumer/ProctorGroupStubber.java index 419fcbb16..94e0fdc2b 100644 --- a/proctor-consumer/src/test/java/com/indeed/proctor/consumer/ProctorGroupStubber.java +++ b/proctor-consumer/src/test/java/com/indeed/proctor/consumer/ProctorGroupStubber.java @@ -48,7 +48,19 @@ public ProctorResultStubBuilder withStubTest( final StubTest stubTest, @Nullable final TestBucket resolved, final TestBucket... definedBuckets) { - withStubTest(stubTest, resolved, stubDefinitionWithVersion("v1", definedBuckets)); + withStubTest(stubTest, resolved, stubDefinitionWithVersion(true, "v1", definedBuckets)); + return this; + } + + public ProctorResultStubBuilder withStubTest( + final boolean forceLogging, + final StubTest stubTest, + @Nullable final TestBucket resolved, + final TestBucket... definedBuckets) { + withStubTest( + stubTest, + resolved, + stubDefinitionWithVersion(forceLogging, "v1", definedBuckets)); return this; } @@ -89,10 +101,11 @@ public ProctorResult build() { } public static ConsumableTestDefinition stubDefinitionWithVersion( - final String version, final TestBucket... buckets) { + final boolean forceLogging, final String version, final TestBucket... buckets) { final ConsumableTestDefinition testDefinition = new ConsumableTestDefinition(); testDefinition.setVersion(version); testDefinition.setBuckets(Arrays.asList(buckets)); + testDefinition.setForceLogging(forceLogging); return testDefinition; } @@ -126,6 +139,7 @@ public enum StubTest implements com.indeed.proctor.consumer.Test { HOLDOUT_MASTER_TEST("holdout_tst", -1), CONTROL_SELECTED_TEST("bgtst", -1), + SUPPRESS_LOGGING_TST("suppress_logging_example_tst", -1), GROUP1_SELECTED_TEST("abtst", -1), GROUP_WITH_FALLBACK_TEST("groupwithfallbacktst", -1), INACTIVE_SELECTED_TEST("btntst", -1), diff --git a/proctor-consumer/src/test/java/com/indeed/proctor/consumer/TestAbstractGroups.java b/proctor-consumer/src/test/java/com/indeed/proctor/consumer/TestAbstractGroups.java index cf1f56735..303b70e6f 100644 --- a/proctor-consumer/src/test/java/com/indeed/proctor/consumer/TestAbstractGroups.java +++ b/proctor-consumer/src/test/java/com/indeed/proctor/consumer/TestAbstractGroups.java @@ -4,16 +4,18 @@ import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.indeed.proctor.common.ProctorResult; -import com.indeed.proctor.common.model.ConsumableTestDefinition; -import com.indeed.proctor.common.model.Payload; +import com.indeed.proctor.common.model.*; import com.indeed.proctor.consumer.ProctorGroupStubber.FakeTest; import com.indeed.proctor.consumer.logging.TestGroupFormatter; import com.indeed.proctor.consumer.logging.TestMarkingObserver; import org.junit.Before; import org.junit.Test; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import static com.indeed.proctor.consumer.AbstractGroups.loggableAllocation; import static com.indeed.proctor.consumer.ProctorGroupStubber.CONTROL_BUCKET_WITH_PAYLOAD; import static com.indeed.proctor.consumer.ProctorGroupStubber.FALLBACK_BUCKET; import static com.indeed.proctor.consumer.ProctorGroupStubber.FALLBACK_NOPAYLOAD_BUCKET; @@ -27,6 +29,7 @@ import static com.indeed.proctor.consumer.ProctorGroupStubber.StubTest.INACTIVE_SELECTED_TEST; import static com.indeed.proctor.consumer.ProctorGroupStubber.StubTest.MISSING_DEFINITION_TEST; import static com.indeed.proctor.consumer.ProctorGroupStubber.StubTest.NO_BUCKETS_WITH_FALLBACK_TEST; +import static com.indeed.proctor.consumer.ProctorGroupStubber.StubTest.SUPPRESS_LOGGING_TST; import static java.util.Collections.emptyMap; import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; @@ -53,6 +56,13 @@ public void setUp() { INACTIVE_BUCKET, CONTROL_BUCKET_WITH_PAYLOAD, GROUP_1_BUCKET_WITH_PAYLOAD) + .withStubTest( + false, + ProctorGroupStubber.StubTest.SUPPRESS_LOGGING_TST, + CONTROL_BUCKET_WITH_PAYLOAD, + INACTIVE_BUCKET, + CONTROL_BUCKET_WITH_PAYLOAD, + GROUP_1_BUCKET_WITH_PAYLOAD) .withStubTest( ProctorGroupStubber.StubTest.GROUP1_SELECTED_TEST, GROUP_1_BUCKET_WITH_PAYLOAD, @@ -164,7 +174,7 @@ public void testToLongString() { assertThat(emptyGroup.toLongString()).isEmpty(); assertThat(sampleGroups.toLongString()) .isEqualTo( - "abtst-group1,bgtst-control,btntst-inactive,groupwithfallbacktst-group1,no_definition_tst-group1"); + "abtst-group1,bgtst-control,btntst-inactive,groupwithfallbacktst-group1,no_definition_tst-group1,suppress_logging_example_tst-control"); } @Test @@ -180,6 +190,106 @@ public void testToLoggingString() { "#A1:abtst1,#A1:bgtst0,#A1:groupwithfallbacktst2,#A1:no_definition_tst2"); } + @Test + public void testToVerifyGroupsString() { + final ConsumableTestDefinition td = + ConsumableTestDefinition.fromTestDefinition( + TestDefinition.builder() + .setTestType(TestType.RANDOM) + .setSalt("foo") + .build()); + final ConsumableTestDefinition tdWithForceLogging = + ConsumableTestDefinition.fromTestDefinition( + TestDefinition.builder() + .setTestType(TestType.RANDOM) + .setSalt("foo") + .setForceLogging(true) + .build()); + final ProctorResult result = + new ProctorGroupStubber.ProctorResultStubBuilder() + .withStubTest( + false, + ProctorGroupStubber.StubTest.SUPPRESS_LOGGING_TST, + CONTROL_BUCKET_WITH_PAYLOAD, + INACTIVE_BUCKET, + CONTROL_BUCKET_WITH_PAYLOAD, + GROUP_1_BUCKET_WITH_PAYLOAD) + .build(); + + assertThat((new AbstractGroups(result) {}).toVerifyGroupsString()) + .isEqualTo("#A1:suppress_logging_example_tst0"); + } + + @Test + public void testCheckRolledOutAllocation() { + final ConsumableTestDefinition td = + ConsumableTestDefinition.fromTestDefinition( + TestDefinition.builder() + .setTestType(TestType.RANDOM) + .setSalt("foo") + .build()); + final ConsumableTestDefinition tdWithForceLogging = + ConsumableTestDefinition.fromTestDefinition( + TestDefinition.builder() + .setTestType(TestType.RANDOM) + .setSalt("foo") + .setForceLogging(true) + .build()); + final ProctorResult result = + new ProctorGroupStubber.ProctorResultStubBuilder() + .withStubTest( + false, + ProctorGroupStubber.StubTest.SUPPRESS_LOGGING_TST, + CONTROL_BUCKET_WITH_PAYLOAD, + INACTIVE_BUCKET, + CONTROL_BUCKET_WITH_PAYLOAD, + GROUP_1_BUCKET_WITH_PAYLOAD) + .build(); + + assertThat(loggableAllocation("suppress_logging_example_tst", tdWithForceLogging, result)) + .isTrue(); + + assertThat(loggableAllocation("suppress_logging_example_tst", td, result)).isFalse(); + } + + @Test + public void testCheckNotRolledOutAllocationAlwaysLogs() { + final ConsumableTestDefinition td = + ConsumableTestDefinition.fromTestDefinition( + TestDefinition.builder() + .setTestType(TestType.RANDOM) + .setSalt("foo") + .build()); + final ConsumableTestDefinition tdWithForceLogging = + ConsumableTestDefinition.fromTestDefinition( + TestDefinition.builder() + .setTestType(TestType.RANDOM) + .setSalt("foo") + .setForceLogging(true) + .build()); + final ProctorResult result = + new ProctorGroupStubber.ProctorResultStubBuilder() + .withStubTest( + false, + SUPPRESS_LOGGING_TST, + CONTROL_BUCKET_WITH_PAYLOAD, + INACTIVE_BUCKET, + CONTROL_BUCKET_WITH_PAYLOAD, + GROUP_1_BUCKET_WITH_PAYLOAD) + .build(); + + final List ranges = new ArrayList<>(); + ranges.add(new Range(0, 0.5)); + ranges.add(new Range(1, 0.5)); + + result.getAllocations().get("suppress_logging_example_tst").setRanges(ranges); + + assertThat(loggableAllocation("suppress_logging_example_tst", tdWithForceLogging, result)) + .isTrue(); + + assertThat(loggableAllocation("suppress_logging_example_tst", td, result)).isTrue(); + } + @Test public void testToLoggingStringWithExposureAndObserver() { @@ -268,11 +378,12 @@ public void testGetJavaScriptConfig() { assertThat(emptyGroup.getJavaScriptConfig()).hasSize(0); assertThat(sampleGroups.getJavaScriptConfig()) - .hasSize(4) + .hasSize(5) .containsEntry(GROUP1_SELECTED_TEST.getName(), 1) .containsEntry(CONTROL_SELECTED_TEST.getName(), 0) .containsEntry(GROUP_WITH_FALLBACK_TEST.getName(), 2) - .containsEntry(MISSING_DEFINITION_TEST.getName(), 2); + .containsEntry(MISSING_DEFINITION_TEST.getName(), 2) + .containsEntry(SUPPRESS_LOGGING_TST.getName(), 0); } @Test