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