Skip to content

Commit 7c06431

Browse files
Full implementation
1 parent c341906 commit 7c06431

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+4300
-544
lines changed

dd-java-agent/agent-featureflag/build.gradle

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,46 @@ plugins {
77
apply from: "$rootDir/gradle/java.gradle"
88
apply from: "$rootDir/gradle/version.gradle"
99

10-
ext {
11-
minJavaVersionForTests = JavaVersion.VERSION_11
12-
}
13-
1410
java {
1511
sourceCompatibility = JavaVersion.VERSION_1_8
1612
targetCompatibility = JavaVersion.VERSION_1_8
1713
}
1814

15+
excludedClassesCoverage += [
16+
// Exposure POJO classes
17+
'com.datadog.featureflag.exposure.Allocation',
18+
'com.datadog.featureflag.exposure.ExposureEvent',
19+
'com.datadog.featureflag.exposure.ExposuresRequest',
20+
'com.datadog.featureflag.exposure.Flag',
21+
'com.datadog.featureflag.exposure.Subject',
22+
'com.datadog.featureflag.exposure.Variant',
23+
// UFC v1 POJO classes
24+
'com.datadog.featureflag.ufc.v1.Allocation',
25+
'com.datadog.featureflag.ufc.v1.ConditionConfiguration',
26+
'com.datadog.featureflag.ufc.v1.ConditionOperator',
27+
'com.datadog.featureflag.ufc.v1.Environment',
28+
'com.datadog.featureflag.ufc.v1.Flag',
29+
'com.datadog.featureflag.ufc.v1.Rule',
30+
'com.datadog.featureflag.ufc.v1.ServerConfiguration',
31+
'com.datadog.featureflag.ufc.v1.Shard',
32+
'com.datadog.featureflag.ufc.v1.ShardRange',
33+
'com.datadog.featureflag.ufc.v1.Split',
34+
'com.datadog.featureflag.ufc.v1.ValueType',
35+
'com.datadog.featureflag.ufc.v1.Variant'
36+
]
37+
1938
dependencies {
2039
api libs.slf4j
2140
implementation libs.moshi
2241
implementation libs.jctools
2342

2443
api project(':dd-trace-api')
44+
compileOnly project(':dd-trace-core')
2545
implementation project(':internal-api')
2646
implementation project(':communication')
2747

2848
testImplementation project(':utils:test-utils')
29-
testImplementation project(':dd-trace-api:openfeature')
30-
testImplementation(libs.openfeature.sdk)
49+
testImplementation project(':dd-java-agent:testing')
3150
}
3251

3352
tasks.named("shadowJar", ShadowJar) {
@@ -38,9 +57,3 @@ tasks.named("jar", Jar) {
3857
archiveClassifier = 'unbundled'
3958
}
4059

41-
[GroovyCompile].each {
42-
tasks.withType(it).configureEach {
43-
configureCompiler(it, 11, JavaVersion.VERSION_11)
44-
}
45-
}
46-

dd-java-agent/agent-featureflag/src/main/java/com/datadog/featureflag/ExposureEvaluatorAdapter.java

Lines changed: 0 additions & 56 deletions
This file was deleted.
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package com.datadog.featureflag;
22

3-
import datadog.trace.api.featureflag.FeatureFlagEvaluator.Context;
4-
import datadog.trace.api.featureflag.FeatureFlagEvaluator.Resolution;
3+
import com.datadog.featureflag.exposure.ExposureEvent;
54

5+
/**
6+
* Defines an exposure writer responsible for sending exposure events to the EVP proxy.
7+
* Implementations should use a background thread to perform these operations asynchronously.
8+
*/
69
public interface ExposureWriter extends AutoCloseable {
710

811
void init();
912

1013
void close();
1114

12-
void write(String flag, Context context, Resolution<?> resolution);
15+
void write(ExposureEvent event);
1316
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package com.datadog.featureflag;
2+
3+
import com.datadog.featureflag.exposure.Allocation;
4+
import com.datadog.featureflag.exposure.ExposureEvent;
5+
import com.datadog.featureflag.exposure.Flag;
6+
import com.datadog.featureflag.exposure.Subject;
7+
import com.datadog.featureflag.exposure.Variant;
8+
import datadog.trace.api.featureflag.FeatureFlagEvaluator;
9+
import datadog.trace.core.util.LRUCache;
10+
import java.util.AbstractMap;
11+
import java.util.Collections;
12+
import java.util.HashMap;
13+
import java.util.Map;
14+
import java.util.Set;
15+
16+
/**
17+
* This class processes evaluations from a {@link FeatureFlagEvaluator} and records the resulting
18+
* exposures through a {@link ExposureWriter}. It uses an LRU cache to avoid sending duplicate
19+
* exposure events.
20+
*/
21+
public class ExposureWriterEvaluatorAdapter implements FeatureFlagEvaluator {
22+
23+
private static final int DEFAULT_CACHE_CAPACITY = 50_000;
24+
25+
private final ExposureWriter writer;
26+
private final FeatureFlagEvaluator delegate;
27+
private final Set<ExposureEvent> cache;
28+
29+
public ExposureWriterEvaluatorAdapter(
30+
final ExposureWriter writer, final FeatureFlagEvaluator delegate) {
31+
this(DEFAULT_CACHE_CAPACITY, writer, delegate);
32+
}
33+
34+
public ExposureWriterEvaluatorAdapter(
35+
final int cacheCapacity, final ExposureWriter writer, final FeatureFlagEvaluator delegate) {
36+
this.writer = writer;
37+
this.delegate = delegate;
38+
this.cache = Collections.newSetFromMap(new LRUCache<>(cacheCapacity));
39+
}
40+
41+
@Override
42+
public Resolution<Boolean> evaluate(
43+
final String key, final Boolean defaultValue, final Context context) {
44+
final Resolution<Boolean> resolution = delegate.evaluate(key, defaultValue, context);
45+
writeResolution(context, resolution);
46+
return resolution;
47+
}
48+
49+
@Override
50+
public Resolution<Integer> evaluate(
51+
final String key, final Integer defaultValue, final Context context) {
52+
final Resolution<Integer> resolution = delegate.evaluate(key, defaultValue, context);
53+
writeResolution(context, resolution);
54+
return resolution;
55+
}
56+
57+
@Override
58+
public Resolution<Double> evaluate(
59+
final String key, final Double defaultValue, final Context context) {
60+
final Resolution<Double> resolution = delegate.evaluate(key, defaultValue, context);
61+
writeResolution(context, resolution);
62+
return resolution;
63+
}
64+
65+
@Override
66+
public Resolution<String> evaluate(
67+
final String key, final String defaultValue, final Context context) {
68+
final Resolution<String> resolution = delegate.evaluate(key, defaultValue, context);
69+
writeResolution(context, resolution);
70+
return resolution;
71+
}
72+
73+
@Override
74+
public Resolution<Object> evaluate(
75+
final String key, final Object defaultValue, final Context context) {
76+
final Resolution<Object> resolution = delegate.evaluate(key, defaultValue, context);
77+
writeResolution(context, resolution);
78+
return resolution;
79+
}
80+
81+
private <E> void writeResolution(final Context context, final Resolution<E> resolution) {
82+
final String allocationKey = allocationKey(resolution);
83+
final String variantKey = resolution.getVariant();
84+
if (allocationKey == null || variantKey == null) {
85+
return;
86+
}
87+
final ExposureEvent event =
88+
new ExposureEvent(
89+
System.currentTimeMillis(),
90+
new Allocation(allocationKey),
91+
new Flag(resolution.getFlagKey()),
92+
new Variant(variantKey),
93+
new Subject(context.getTargetingKey(), flattenContext(context)));
94+
95+
boolean writeEvent;
96+
synchronized (cache) {
97+
writeEvent = cache.add(event);
98+
}
99+
if (writeEvent) {
100+
writer.write(event);
101+
}
102+
}
103+
104+
private AbstractMap<String, Object> flattenContext(final Context context) {
105+
if (context == null) {
106+
return null;
107+
}
108+
final Set<String> keys = context.keySet();
109+
final HashMap<String, Object> result = new HashMap<>(keys.size());
110+
for (final String key : keys) {
111+
final Object value = context.getValue(key);
112+
// TODO ignore nested elements for now as they are not supported by the backend
113+
if (value instanceof Integer
114+
|| value instanceof Double
115+
|| value instanceof Boolean
116+
|| value instanceof String) {
117+
result.put(key, value);
118+
} else if (value == null) {
119+
result.put(key, null);
120+
}
121+
}
122+
return result;
123+
}
124+
125+
private static String allocationKey(final Resolution<?> resolution) {
126+
final Map<String, Object> meta = resolution.getFlagMetadata();
127+
return meta == null ? null : (String) meta.get("allocationKey");
128+
}
129+
}

dd-java-agent/agent-featureflag/src/main/java/com/datadog/featureflag/ExposureWriterImpl.java

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,9 @@
88
import com.squareup.moshi.JsonAdapter;
99
import com.squareup.moshi.Moshi;
1010
import datadog.communication.ddagent.DDAgentFeaturesDiscovery;
11-
import datadog.communication.ddagent.SharedCommunicationObjects;
1211
import datadog.communication.http.HttpRetryPolicy;
1312
import datadog.communication.http.OkHttpUtils;
1413
import datadog.trace.api.Config;
15-
import datadog.trace.api.featureflag.FeatureFlagEvaluator.Context;
16-
import datadog.trace.api.featureflag.FeatureFlagEvaluator.Resolution;
1714
import java.util.ArrayList;
1815
import java.util.HashMap;
1916
import java.util.List;
@@ -30,7 +27,7 @@
3027

3128
public class ExposureWriterImpl implements ExposureWriter {
3229

33-
private static final String EXPOSURES_API_PATH = "/api/v2/exposures";
30+
private static final String EXPOSURES_API_PATH = "api/v2/exposures";
3431
private static final String EVP_SUBDOMAIN_HEADER_NAME = "X-Datadog-EVP-Subdomain";
3532
private static final String EVP_SUBDOMAIN_HEADER_VALUE = "event-platform-intake";
3633

@@ -43,13 +40,13 @@ public ExposureWriterImpl(
4340
final int capacity,
4441
final long flushInterval,
4542
final TimeUnit timeUnit,
46-
final SharedCommunicationObjects sco,
47-
Config config) {
43+
final HttpUrl agentUrl,
44+
final Config config) {
4845
this.queue = new MpscBlockingConsumerArrayQueue<>(capacity);
4946
final Headers headers = Headers.of(EVP_SUBDOMAIN_HEADER_NAME, EVP_SUBDOMAIN_HEADER_VALUE);
5047
final HttpUrl url =
5148
HttpUrl.get(
52-
sco.agentUrl.toString()
49+
agentUrl.toString()
5350
+ DDAgentFeaturesDiscovery.V2_EVP_PROXY_ENDPOINT
5451
+ EXPOSURES_API_PATH);
5552
final Map<String, String> context = new HashMap<>();
@@ -71,11 +68,13 @@ public void init() {
7168
}
7269

7370
@Override
74-
public void close() {}
71+
public void close() {
72+
this.serializerThread.interrupt();
73+
}
7574

7675
@Override
77-
public void write(final String flag, final Context context, final Resolution<?> resolution) {
78-
final long timestamp = System.currentTimeMillis();
76+
public void write(final ExposureEvent event) {
77+
queue.offer(event);
7978
}
8079

8180
private static class ExposureSerializingHandler implements Runnable {
@@ -122,8 +121,8 @@ public void run() {
122121
Thread.currentThread().interrupt();
123122
}
124123
log.debug(
125-
"exposure processor worker exited. submitting explosures stopped. unsubmitted evals left: "
126-
+ !queuesAreEmpty());
124+
"exposure processor worker exited. submitting exposures stopped. unsubmitted exposures left: {}",
125+
!queuesAreEmpty());
127126
}
128127

129128
private void runDutyCycle() throws InterruptedException {

0 commit comments

Comments
 (0)