Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions sdk-java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@
<artifactId>sdk-java</artifactId>

<dependencies>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>${protobuf.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
Expand All @@ -27,6 +21,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>dev.failsafe</groupId>
<artifactId>failsafe</artifactId>
Expand All @@ -37,6 +36,12 @@
<artifactId>proto-google-common-protos</artifactId>
<version>${common.protos.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
41 changes: 30 additions & 11 deletions sdk-java/src/main/java/com/spotify/confidence/Confidence.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,24 @@
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;
import com.spotify.confidence.Exceptions.IncompatibleValueType;
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;
Expand All @@ -41,6 +43,7 @@ public abstract class Confidence implements FlagEvaluator, EventSender, Closeabl

protected Map<String, ConfidenceValue> 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
Expand Down Expand Up @@ -137,15 +140,7 @@ public <T> FlagEvaluation<T> 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(
Expand Down Expand Up @@ -201,6 +196,30 @@ public <T> FlagEvaluation<T> 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<ResolveFlagsResponse> resolveFlags(String flagName) {
return client().resolveFlags(flagName, getContext());
}
Expand Down
14 changes: 14 additions & 0 deletions sdk-java/src/main/proto/confidence/internal.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
syntax = "proto3";

import "google/protobuf/struct.proto";

package confidence.internal.v1;
option java_package = "com.spotify.internal.v1";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit sad we need to leak this naming, but not a big deal

option java_multiple_files = true;
option java_outer_classname = "SdkLogging";

message ResolveTesterLogging {
string client_key = 1;
string flag = 2;
google.protobuf.Value context = 3;
}
35 changes: 35 additions & 0 deletions sdk-java/src/test/java/com/spotify/confidence/ConfidenceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ILoggingEvent> 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
Expand Down Expand Up @@ -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<ILoggingEvent> 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
Expand Down