Skip to content

Commit f7c6036

Browse files
Add FFE evaluation java weblogs
1 parent a456ba8 commit f7c6036

File tree

5 files changed

+311
-3
lines changed

5 files changed

+311
-3
lines changed

manifests/java.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2093,7 +2093,7 @@ tests/:
20932093
parametric/:
20942094
test_feature_flag_exposure/:
20952095
test_feature_flag_exposure.py:
2096-
Test_Feature_Flag_Exposure: missing_feature
2096+
Test_Feature_Flag_Exposure: v1.55.0-SNAPSHOT
20972097
test_128_bit_traceids.py:
20982098
Test_128_Bit_Traceids: v1.12.0
20992099
test_config_consistency.py:
@@ -2130,7 +2130,7 @@ tests/:
21302130
Test_Parametric_DDTrace_Baggage: incomplete_test_app (baggage endpoints are not implemented)
21312131
Test_Parametric_DDTrace_Crash: bug (APMAPI-778) # The crash endpoint does not kill the application
21322132
Test_Parametric_DDTrace_Current_Span: bug (APMAPI-778) # Fails to retreive the current span after a span has finished
2133-
Test_Parametric_FFE_Start: missing_feature
2133+
Test_Parametric_FFE_Start: v1.55.0-SNAPSHOT
21342134
Test_Parametric_Otel_Baggage: missing_feature (otel baggage is not supported)
21352135
Test_Parametric_Otel_Current_Span: bug (APMAPI-778) # Current span endpoint does not return DataDog spans created by the otel api
21362136
Test_Parametric_Write_Log: missing_feature
@@ -2311,7 +2311,7 @@ tests/:
23112311
Test_Span_Links_From_Conflicting_Contexts: v1.43.0
23122312
Test_Span_Links_Omit_Tracestate_From_Conflicting_Contexts: missing_feature (implementation specs have not been determined)
23132313
test_feature_flag_exposures.py:
2314-
Test_FFE_Exposure_Events: missing_feature
2314+
Test_FFE_Exposure_Events: v1.55.0-SNAPSHOT
23152315
test_graphql.py:
23162316
Test_GraphQLOperationErrorReporting:
23172317
'*': missing_feature

utils/build/docker/java/parametric/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@
8989
<artifactId>javax.mail</artifactId>
9090
<version>1.6.2</version>
9191
</dependency>
92+
<dependency>
93+
<groupId>dev.openfeature</groupId>
94+
<artifactId>sdk</artifactId>
95+
<version>1.18.2</version>
96+
</dependency>
9297
</dependencies>
9398

9499
<build>
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package com.datadoghq.trace.controller;
2+
3+
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
4+
5+
import com.fasterxml.jackson.annotation.JsonAlias;
6+
import datadog.trace.api.openfeature.Provider;
7+
import dev.openfeature.sdk.Client;
8+
import dev.openfeature.sdk.EvaluationContext;
9+
import dev.openfeature.sdk.MutableContext;
10+
import dev.openfeature.sdk.OpenFeatureAPI;
11+
import dev.openfeature.sdk.Structure;
12+
import dev.openfeature.sdk.Value;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.web.bind.annotation.PostMapping;
17+
import org.springframework.web.bind.annotation.RequestBody;
18+
import org.springframework.web.bind.annotation.RequestMapping;
19+
import org.springframework.web.bind.annotation.RestController;
20+
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
@RestController
26+
@RequestMapping("/ffe")
27+
public class FeatureFlagEvaluatorController {
28+
29+
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlagEvaluatorController.class);
30+
31+
@PostMapping(value = "/start")
32+
public ResponseEntity<Boolean> start() {
33+
OpenFeatureAPI.getInstance().setProviderAndWait(new Provider());
34+
return ResponseEntity.ok(true);
35+
}
36+
37+
@PostMapping(value = "/evaluate", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
38+
public ResponseEntity<Map<String, Object>> evaluate(@RequestBody final EvaluateRequest request) {
39+
Object value;
40+
String reason;
41+
final EvaluationContext context = context(request);
42+
final Client client = OpenFeatureAPI.getInstance().getClient();
43+
try {
44+
value = switch (request.getVariationType()) {
45+
case "BOOLEAN" ->
46+
client.getBooleanValue(request.getFlag(), (Boolean) request.getDefaultValue(), context);
47+
case "STRING" -> client.getStringValue(request.getFlag(), (String) request.getDefaultValue(), context);
48+
case "INTEGER" -> {
49+
final Number integerEval = (Number) request.getDefaultValue();
50+
yield client.getIntegerValue(request.getFlag(), integerEval.intValue(), context);
51+
}
52+
case "NUMERIC" -> {
53+
final Number doubleEval = (Number) request.getDefaultValue();
54+
yield client.getDoubleValue(request.getFlag(), doubleEval.doubleValue(), context);
55+
}
56+
case "JSON" -> {
57+
final Value objectValue = client.getObjectValue(request.getFlag(), Value.objectToValue(request.getDefaultValue()), context);
58+
yield context.convertValue(objectValue);
59+
}
60+
default -> request.getDefaultValue();
61+
};
62+
63+
reason = "DEFAULT";
64+
} catch (Throwable e) {
65+
LOGGER.error("Error on resolution", e);
66+
value = request.getDefaultValue();
67+
reason = "ERROR";
68+
}
69+
final Map<String, Object> result = new HashMap<>();
70+
result.put("reason", reason);
71+
result.put("value", value);
72+
return ResponseEntity.ok(result);
73+
}
74+
75+
private static EvaluationContext context(final EvaluateRequest request) {
76+
final MutableContext context = new MutableContext();
77+
context.setTargetingKey(request.getTargetingKey());
78+
request.attributes.forEach((key, value) -> {
79+
if (value instanceof Boolean) {
80+
context.add(key, (Boolean) value);
81+
} else if (value instanceof Integer) {
82+
context.add(key, (Integer) value);
83+
} else if (value instanceof Double) {
84+
context.add(key, (Double) value);
85+
} else if (value instanceof String) {
86+
context.add(key, (String) value);
87+
} else if (value instanceof Map) {
88+
context.add(key, Value.objectToValue(value).asStructure());
89+
} else if (value instanceof List) {
90+
context.add(key, Value.objectToValue(value).asList());
91+
} else {
92+
context.add(key, (Structure) null);
93+
}
94+
});
95+
return context;
96+
}
97+
98+
public static class EvaluateRequest {
99+
private String flag;
100+
@JsonAlias("variation_type")
101+
private String variationType;
102+
@JsonAlias("default_value")
103+
private Object defaultValue;
104+
@JsonAlias("targeting_key")
105+
private String targetingKey;
106+
private Map<String, Object> attributes;
107+
108+
public Map<String, Object> getAttributes() {
109+
return attributes;
110+
}
111+
112+
public void setAttributes(Map<String, Object> attributes) {
113+
this.attributes = attributes;
114+
}
115+
116+
public Object getDefaultValue() {
117+
return defaultValue;
118+
}
119+
120+
public void setDefaultValue(Object defaultValue) {
121+
this.defaultValue = defaultValue;
122+
}
123+
124+
public String getFlag() {
125+
return flag;
126+
}
127+
128+
public void setFlag(String flag) {
129+
this.flag = flag;
130+
}
131+
132+
public String getTargetingKey() {
133+
return targetingKey;
134+
}
135+
136+
public void setTargetingKey(String targetingKey) {
137+
this.targetingKey = targetingKey;
138+
}
139+
140+
public String getVariationType() {
141+
return variationType;
142+
}
143+
144+
public void setVariationType(String variationType) {
145+
this.variationType = variationType;
146+
}
147+
}
148+
}

utils/build/docker/java/spring-boot/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@
224224
<artifactId>okhttp</artifactId>
225225
<version>4.9.3</version>
226226
</dependency>
227+
<dependency>
228+
<groupId>dev.openfeature</groupId>
229+
<artifactId>sdk</artifactId>
230+
<version>1.18.2</version>
231+
</dependency>
227232
</dependencies>
228233

229234
<dependencyManagement>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package com.datadoghq.system_tests.springboot.featureflag;
2+
3+
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
4+
5+
import datadog.trace.api.openfeature.Provider;
6+
import dev.openfeature.sdk.Client;
7+
import dev.openfeature.sdk.EvaluationContext;
8+
import dev.openfeature.sdk.MutableContext;
9+
import dev.openfeature.sdk.OpenFeatureAPI;
10+
import dev.openfeature.sdk.Structure;
11+
import dev.openfeature.sdk.Value;
12+
import org.slf4j.Logger;
13+
import org.slf4j.LoggerFactory;
14+
import org.springframework.http.ResponseEntity;
15+
import org.springframework.web.bind.annotation.PostMapping;
16+
import org.springframework.web.bind.annotation.RequestBody;
17+
import org.springframework.web.bind.annotation.RestController;
18+
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
@RestController
24+
public class FeatureFlagEvaluatorController {
25+
26+
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlagEvaluatorController.class);
27+
28+
private final Client client;
29+
30+
public FeatureFlagEvaluatorController() {
31+
final OpenFeatureAPI api = OpenFeatureAPI.getInstance();
32+
api.setProviderAndWait(new Provider());
33+
client = api.getClient();
34+
}
35+
36+
@PostMapping(value = "/ffe", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
37+
public ResponseEntity<Map<String, Object>> evaluate(@RequestBody final EvaluateRequest request) {
38+
Object value;
39+
String reason;
40+
final EvaluationContext context = context(request);
41+
try {
42+
switch (request.getVariationType()) {
43+
case "BOOLEAN":
44+
value = client.getBooleanValue(request.getFlag(), (Boolean) request.getDefaultValue(), context);
45+
break;
46+
case "STRING":
47+
value = client.getStringValue(request.getFlag(), (String) request.getDefaultValue(), context);
48+
break;
49+
case "INTEGER":
50+
value = client.getIntegerValue(request.getFlag(), (Integer) request.getDefaultValue(), context);
51+
break;
52+
case "NUMERIC":
53+
final Number number = (Number) request.getDefaultValue();
54+
if (number instanceof Double) {
55+
value = client.getDoubleValue(request.getFlag(), number.doubleValue(), context);
56+
} else {
57+
value = client.getIntegerValue(request.getFlag(), number.intValue(), context);
58+
}
59+
break;
60+
case "JSON":
61+
final Value objectValue = client.getObjectValue(request.getFlag(), Value.objectToValue(request.getDefaultValue()), context);
62+
value = context.convertValue(objectValue);
63+
break;
64+
default:
65+
value = request.getDefaultValue();
66+
}
67+
68+
reason = "DEFAULT";
69+
} catch (Throwable e) {
70+
LOGGER.error("Error on resolution", e);
71+
value = request.getDefaultValue();
72+
reason = "ERROR";
73+
}
74+
final Map<String, Object> result = new HashMap<>();
75+
result.put("reason", reason);
76+
result.put("value", value);
77+
return ResponseEntity.ok(result);
78+
}
79+
80+
private static EvaluationContext context(final EvaluateRequest request) {
81+
final MutableContext context = new MutableContext();
82+
context.setTargetingKey(request.getTargetingKey());
83+
request.attributes.forEach((key, value) -> {
84+
if (value instanceof Boolean) {
85+
context.add(key, (Boolean) value);
86+
} else if (value instanceof Integer) {
87+
context.add(key, (Integer) value);
88+
} else if (value instanceof Double) {
89+
context.add(key, (Double) value);
90+
} else if (value instanceof String) {
91+
context.add(key, (String) value);
92+
} else if (value instanceof Map) {
93+
context.add(key, Value.objectToValue(value).asStructure());
94+
} else if (value instanceof List) {
95+
context.add(key, Value.objectToValue(value).asList());
96+
} else {
97+
context.add(key, (Structure) null);
98+
}
99+
});
100+
return context;
101+
}
102+
103+
public static class EvaluateRequest {
104+
private String flag;
105+
private String variationType;
106+
private Object defaultValue;
107+
private String targetingKey;
108+
private Map<String, Object> attributes;
109+
110+
public Map<String, Object> getAttributes() {
111+
return attributes;
112+
}
113+
114+
public void setAttributes(Map<String, Object> attributes) {
115+
this.attributes = attributes;
116+
}
117+
118+
public Object getDefaultValue() {
119+
return defaultValue;
120+
}
121+
122+
public void setDefaultValue(Object defaultValue) {
123+
this.defaultValue = defaultValue;
124+
}
125+
126+
public String getFlag() {
127+
return flag;
128+
}
129+
130+
public void setFlag(String flag) {
131+
this.flag = flag;
132+
}
133+
134+
public String getTargetingKey() {
135+
return targetingKey;
136+
}
137+
138+
public void setTargetingKey(String targetingKey) {
139+
this.targetingKey = targetingKey;
140+
}
141+
142+
public String getVariationType() {
143+
return variationType;
144+
}
145+
146+
public void setVariationType(String variationType) {
147+
this.variationType = variationType;
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)