diff --git a/sdk-java/pom.xml b/sdk-java/pom.xml
index a11f39c2..07130669 100644
--- a/sdk-java/pom.xml
+++ b/sdk-java/pom.xml
@@ -10,12 +10,6 @@
sdk-java
-
- com.google.protobuf
- protobuf-java-util
- ${protobuf.version}
- test
-
io.grpc
grpc-protobuf
@@ -27,6 +21,11 @@
+
+ com.google.protobuf
+ protobuf-java-util
+ ${protobuf.version}
+
dev.failsafe
failsafe
@@ -37,6 +36,12 @@
proto-google-common-protos
${common.protos.version}
+
+ ch.qos.logback
+ logback-classic
+ 1.4.14
+ test
+
diff --git a/sdk-java/src/main/java/com/spotify/confidence/Confidence.java b/sdk-java/src/main/java/com/spotify/confidence/Confidence.java
index 4611315a..1b5d083b 100644
--- a/sdk-java/src/main/java/com/spotify/confidence/Confidence.java
+++ b/sdk-java/src/main/java/com/spotify/confidence/Confidence.java
@@ -9,6 +9,8 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.io.Closer;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
import com.spotify.confidence.ConfidenceUtils.FlagPath;
import com.spotify.confidence.Exceptions.IllegalValuePath;
import com.spotify.confidence.Exceptions.IllegalValueType;
@@ -16,15 +18,15 @@
import com.spotify.confidence.Exceptions.ValueNotFound;
import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsResponse;
import com.spotify.confidence.shaded.flags.resolver.v1.ResolvedFlag;
+import com.spotify.internal.v1.ResolveTesterLogging;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import java.io.Closeable;
import java.io.IOException;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
+import java.util.Base64;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -41,6 +43,7 @@ public abstract class Confidence implements FlagEvaluator, EventSender, Closeabl
protected Map context = Maps.newHashMap();
private static final Logger log = org.slf4j.LoggerFactory.getLogger(Confidence.class);
+ private static final JsonFormat.Printer jsonPrinter = JsonFormat.printer();
protected Confidence() {
// Protected constructor to allow subclassing
@@ -137,15 +140,7 @@ public FlagEvaluation getEvaluation(String key, T defaultValue) {
}
final ResolvedFlag resolvedFlag = response.getResolvedFlags(0);
- final String clientKey = client().clientSecret;
- final String flag = resolvedFlag.getFlag();
- final String context = URLEncoder.encode(getContext().toString(), StandardCharsets.UTF_8);
- final String logMessage =
- String.format(
- "See resolves for '%s' in Confidence: "
- + "https://app.confidence.spotify.com/flags/resolver-test?client-key=%s&flag=flags/%s&context=%s",
- flag, clientKey, flag, context);
- log.debug(logMessage);
+ logResolveTesterHint(resolvedFlag);
if (!requestFlagName.equals(resolvedFlag.getFlag())) {
final String errorMessage =
String.format(
@@ -201,6 +196,30 @@ public FlagEvaluation getEvaluation(String key, T defaultValue) {
}
}
+ @VisibleForTesting
+ public void logResolveTesterHint(ResolvedFlag resolvedFlag) {
+ final String clientKey = client().clientSecret;
+ final String flag = resolvedFlag.getFlag();
+ try {
+ final ResolveTesterLogging resolveTesterLogging =
+ ResolveTesterLogging.newBuilder()
+ .setClientKey(clientKey)
+ .setFlag(flag)
+ .setContext(getContext().toProto())
+ .build();
+ final String base64 =
+ Base64.getEncoder().encodeToString(jsonPrinter.print(resolveTesterLogging).getBytes());
+ final String logMessage =
+ String.format(
+ "Check your flag evaluation for '%s' by copy pasting the payload to the Resolve tester '%s'",
+ flag, base64);
+ log.debug(logMessage);
+ } catch (InvalidProtocolBufferException e) {
+ log.warn("Failed to produce correct resolve tester content", e);
+ // warn and ignore is enough
+ }
+ }
+
CompletableFuture resolveFlags(String flagName) {
return client().resolveFlags(flagName, getContext());
}
diff --git a/sdk-java/src/main/proto/confidence/internal.proto b/sdk-java/src/main/proto/confidence/internal.proto
new file mode 100644
index 00000000..c8e7f74c
--- /dev/null
+++ b/sdk-java/src/main/proto/confidence/internal.proto
@@ -0,0 +1,14 @@
+syntax = "proto3";
+
+import "google/protobuf/struct.proto";
+
+package confidence.internal.v1;
+option java_package = "com.spotify.internal.v1";
+option java_multiple_files = true;
+option java_outer_classname = "SdkLogging";
+
+message ResolveTesterLogging {
+ string client_key = 1;
+ string flag = 2;
+ google.protobuf.Value context = 3;
+}
\ No newline at end of file
diff --git a/sdk-java/src/test/java/com/spotify/confidence/ConfidenceTest.java b/sdk-java/src/test/java/com/spotify/confidence/ConfidenceTest.java
index 608c6306..2594165f 100644
--- a/sdk-java/src/test/java/com/spotify/confidence/ConfidenceTest.java
+++ b/sdk-java/src/test/java/com/spotify/confidence/ConfidenceTest.java
@@ -2,6 +2,10 @@
import static org.junit.jupiter.api.Assertions.*;
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
import com.google.protobuf.Value;
import com.spotify.confidence.ConfidenceValue.Struct;
import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsResponse;
@@ -15,18 +19,33 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
final class ConfidenceTest {
private final FakeEventSenderEngine fakeEngine = new FakeEventSenderEngine(new FakeClock());
private final ResolverClientTestUtils.FakeFlagResolverClient fakeFlagResolverClient =
new ResolverClientTestUtils.FakeFlagResolverClient();
private static Confidence confidence;
+ private ListAppender listAppender;
+ private Logger confidenceLogger;
@BeforeEach
void beforeEach() {
confidence = Confidence.create(fakeEngine, fakeFlagResolverClient, "clientKey");
+ confidenceLogger = (Logger) LoggerFactory.getLogger(Confidence.class);
+
+ listAppender = new ListAppender<>();
+ listAppender.start();
+ // Add the appender to the logger
+ confidenceLogger.addAppender(listAppender);
+ }
+
+ @AfterEach
+ void afterEach() {
+ confidenceLogger.detachAppender(listAppender);
}
@Test
@@ -259,6 +278,22 @@ void internalError() {
evaluation.getErrorMessage().get().startsWith("Crashing while performing network call"));
}
+ @Test
+ void shouldLogResolverHint() {
+ confidence
+ .withContext(Map.of("my_context_value", ConfidenceValue.of(42)))
+ .logResolveTesterHint(ResolvedFlag.newBuilder().setFlag("FlagName").build());
+ final List loggingEvents = listAppender.list;
+ assertTrue(loggingEvents.size() > 0, "No log message was captured.");
+ final ILoggingEvent lastLogEvent = loggingEvents.get(loggingEvents.size() - 1);
+ assertEquals(Level.DEBUG, lastLogEvent.getLevel()); // Or whatever level you expect
+ assertEquals(
+ "Check your flag evaluation for 'FlagName' by copy pasting the payload to the Resolve tester "
+ + "'ewogICJjbGllbnRLZXkiOiAiY2xpZW50S2V5IiwKICAiZmxhZyI6ICJGbGFnTmFtZSIsCiAgImN"
+ + "vbnRleHQiOiB7CiAgICAibXlfY29udGV4dF92YWx1ZSI6IDQyLjAKICB9Cn0='",
+ lastLogEvent.getFormattedMessage());
+ }
+
public static class FailingFlagResolverClient implements FlagResolverClient {
@Override