Skip to content

Commit 5349db8

Browse files
committed
feat: add telemetry helper utils
Signed-off-by: liran2000 <[email protected]>
1 parent de64edd commit 5349db8

File tree

3 files changed

+359
-0
lines changed

3 files changed

+359
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package dev.openfeature.sdk;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.Singular;
8+
9+
/**
10+
* Represents an evaluation event.
11+
*/
12+
@Builder
13+
@Getter
14+
public class EvaluationEvent {
15+
16+
private String name;
17+
18+
@Singular("attribute")
19+
private Map<String, Object> attributes;
20+
21+
@Singular("bodyElement")
22+
private Map<String, Object> body;
23+
24+
public Map<String, Object> getAttributes() {
25+
return new HashMap<>(attributes);
26+
}
27+
28+
public Map<String, Object> getBody() {
29+
return new HashMap<>(body);
30+
}
31+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package dev.openfeature.sdk;
2+
3+
/**
4+
* The Telemetry class provides constants and methods for creating OpenTelemetry compliant
5+
* evaluation events.
6+
*/
7+
public class Telemetry {
8+
9+
/*
10+
The OpenTelemetry compliant event attributes for flag evaluation.
11+
Specification: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
12+
*/
13+
public static final String TELEMETRY_KEY = "feature_flag.key";
14+
public static final String TELEMETRY_ERROR_CODE = "error.type";
15+
public static final String TELEMETRY_VARIANT = "feature_flag.variant";
16+
public static final String TELEMETRY_CONTEXT_ID = "feature_flag.context.id";
17+
public static final String TELEMETRY_ERROR_MSG = "feature_flag.evaluation.error.message";
18+
public static final String TELEMETRY_REASON = "feature_flag.evaluation.reason";
19+
public static final String TELEMETRY_PROVIDER = "feature_flag.provider_name";
20+
public static final String TELEMETRY_FLAG_SET_ID = "feature_flag.set.id";
21+
public static final String TELEMETRY_VERSION = "feature_flag.version";
22+
23+
// Well-known flag metadata attributes for telemetry events.
24+
// Specification: https://openfeature.dev/specification/appendix-d#flag-metadata
25+
public static final String TELEMETRY_FLAG_META_CONTEXT_ID = "contextId";
26+
public static final String TELEMETRY_FLAG_META_FLAG_SET_ID = "flagSetId";
27+
public static final String TELEMETRY_FLAG_META_VERSION = "version";
28+
29+
// OpenTelemetry event body.
30+
// Specification: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
31+
public static final String TELEMETRY_BODY = "value";
32+
33+
public static final String FLAG_EVALUATION_EVENT_NAME = "feature_flag.evaluation";
34+
35+
/**
36+
* Creates an EvaluationEvent using the provided HookContext and ProviderEvaluation.
37+
*
38+
* @param hookContext the context containing flag evaluation details
39+
* @param providerEvaluation the evaluation result from the provider
40+
*
41+
* @return an EvaluationEvent populated with telemetry data
42+
*/
43+
public static EvaluationEvent createEvaluationEvent(HookContext hookContext,
44+
ProviderEvaluation<?> providerEvaluation) {
45+
EvaluationEvent.EvaluationEventBuilder evaluationEventBuilder = EvaluationEvent.builder()
46+
.name(FLAG_EVALUATION_EVENT_NAME)
47+
.attribute(TELEMETRY_KEY, hookContext.getFlagKey())
48+
.attribute(TELEMETRY_PROVIDER, hookContext.getProviderMetadata().getName());
49+
50+
if (providerEvaluation.getReason() != null) {
51+
evaluationEventBuilder.attribute(TELEMETRY_REASON, providerEvaluation.getReason().toLowerCase());
52+
} else {
53+
evaluationEventBuilder.attribute(TELEMETRY_REASON, Reason.UNKNOWN.name().toLowerCase());
54+
}
55+
56+
if (providerEvaluation.getVariant() != null) {
57+
evaluationEventBuilder.attribute(TELEMETRY_VARIANT, providerEvaluation.getVariant());
58+
} else {
59+
evaluationEventBuilder.bodyElement(TELEMETRY_BODY, providerEvaluation.getValue());
60+
}
61+
62+
String contextId = providerEvaluation.getFlagMetadata().getString(TELEMETRY_FLAG_META_CONTEXT_ID);
63+
if (contextId != null) {
64+
evaluationEventBuilder.attribute(TELEMETRY_CONTEXT_ID, contextId);
65+
} else {
66+
evaluationEventBuilder.attribute(TELEMETRY_CONTEXT_ID, hookContext.getCtx().getTargetingKey());
67+
}
68+
69+
String setID = providerEvaluation.getFlagMetadata().getString(TELEMETRY_FLAG_META_FLAG_SET_ID);
70+
if (setID != null) {
71+
evaluationEventBuilder.attribute(TELEMETRY_FLAG_SET_ID, setID);
72+
}
73+
74+
String version = providerEvaluation.getFlagMetadata().getString(TELEMETRY_FLAG_META_VERSION);
75+
if (version != null) {
76+
evaluationEventBuilder.attribute(TELEMETRY_VERSION, version);
77+
}
78+
79+
if (Reason.ERROR.name().equals(providerEvaluation.getReason())) {
80+
if (providerEvaluation.getErrorCode() != null) {
81+
evaluationEventBuilder.attribute(TELEMETRY_ERROR_CODE, providerEvaluation.getErrorCode());
82+
} else {
83+
evaluationEventBuilder.attribute(TELEMETRY_ERROR_CODE, ErrorCode.GENERAL);
84+
}
85+
86+
if (providerEvaluation.getErrorMessage() != null) {
87+
evaluationEventBuilder.attribute(TELEMETRY_ERROR_MSG, providerEvaluation.getErrorMessage());
88+
}
89+
}
90+
91+
return evaluationEventBuilder.build();
92+
}
93+
}
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package dev.openfeature.sdk;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNull;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.when;
7+
8+
import org.junit.jupiter.api.Test;
9+
10+
public class TelemetryTest {
11+
12+
@Test
13+
public void testCreatesEvaluationEventWithMandatoryFields() {
14+
// Arrange
15+
String flagKey = "test-flag";
16+
String providerName = "test-provider";
17+
String reason = "static";
18+
19+
Metadata providerMetadata = mock(Metadata.class);
20+
when(providerMetadata.getName()).thenReturn(providerName);
21+
22+
HookContext<Boolean> hookContext = HookContext.<Boolean>builder()
23+
.flagKey(flagKey)
24+
.providerMetadata(providerMetadata)
25+
.type(FlagValueType.BOOLEAN)
26+
.defaultValue(false)
27+
.ctx(new ImmutableContext())
28+
.build();
29+
30+
ProviderEvaluation<Boolean> evaluation = ProviderEvaluation.<Boolean>builder()
31+
.reason(reason)
32+
.value(true)
33+
.build();
34+
35+
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, evaluation);
36+
37+
assertEquals(Telemetry.FLAG_EVALUATION_EVENT_NAME, event.getName());
38+
assertEquals(flagKey, event.getAttributes().get(Telemetry.TELEMETRY_KEY));
39+
assertEquals(providerName, event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER));
40+
assertEquals(reason.toLowerCase(), event.getAttributes().get(Telemetry.TELEMETRY_REASON));
41+
}
42+
43+
@Test
44+
public void testHandlesNullReason() {
45+
// Arrange
46+
String flagKey = "test-flag";
47+
String providerName = "test-provider";
48+
49+
Metadata providerMetadata = mock(Metadata.class);
50+
when(providerMetadata.getName()).thenReturn(providerName);
51+
52+
HookContext<Boolean> hookContext = HookContext.<Boolean>builder()
53+
.flagKey(flagKey)
54+
.providerMetadata(providerMetadata)
55+
.type(FlagValueType.BOOLEAN)
56+
.defaultValue(false)
57+
.ctx(new ImmutableContext())
58+
.build();
59+
60+
ProviderEvaluation<Boolean> evaluation = ProviderEvaluation.<Boolean>builder()
61+
.reason(null)
62+
.value(true)
63+
.build();
64+
65+
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, evaluation);
66+
67+
assertEquals(Reason.UNKNOWN.name().toLowerCase(), event.getAttributes().get(Telemetry.TELEMETRY_REASON));
68+
}
69+
70+
@Test
71+
public void testSetsVariantAttributeWhenVariantExists() {
72+
HookContext<String> hookContext = HookContext.<String>builder()
73+
.flagKey("testFlag")
74+
.type(FlagValueType.STRING)
75+
.defaultValue("default")
76+
.ctx(mock(EvaluationContext.class))
77+
.clientMetadata(mock(ClientMetadata.class))
78+
.providerMetadata(mock(Metadata.class))
79+
.build();
80+
81+
ProviderEvaluation<String> providerEvaluation = ProviderEvaluation.<String>builder()
82+
.variant("testVariant")
83+
.flagMetadata(ImmutableMetadata.builder().build())
84+
.build();
85+
86+
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation);
87+
88+
assertEquals("testVariant", event.getAttributes().get(Telemetry.TELEMETRY_VARIANT));
89+
}
90+
91+
@Test
92+
public void test_sets_value_in_body_when_variant_is_null() {
93+
HookContext<String> hookContext = HookContext.<String>builder()
94+
.flagKey("testFlag")
95+
.type(FlagValueType.STRING)
96+
.defaultValue("default")
97+
.ctx(mock(EvaluationContext.class))
98+
.clientMetadata(mock(ClientMetadata.class))
99+
.providerMetadata(mock(Metadata.class))
100+
.build();
101+
102+
ProviderEvaluation<String> providerEvaluation = ProviderEvaluation.<String>builder()
103+
.value("testValue")
104+
.flagMetadata(ImmutableMetadata.builder().build())
105+
.build();
106+
107+
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation);
108+
109+
assertEquals("testValue", event.getBody().get(Telemetry.TELEMETRY_BODY));
110+
}
111+
112+
@Test
113+
public void testAllFieldsPopulated() {
114+
EvaluationContext evaluationContext = mock(EvaluationContext.class);
115+
when(evaluationContext.getTargetingKey()).thenReturn("realTargetingKey");
116+
117+
Metadata providerMetadata = mock(Metadata.class);
118+
when(providerMetadata.getName()).thenReturn("realProviderName");
119+
120+
HookContext<String> hookContext = HookContext.<String>builder()
121+
.flagKey("realFlag")
122+
.type(FlagValueType.STRING)
123+
.defaultValue("realDefault")
124+
.ctx(evaluationContext)
125+
.clientMetadata(mock(ClientMetadata.class))
126+
.providerMetadata(providerMetadata)
127+
.build();
128+
129+
ProviderEvaluation<String> providerEvaluation = ProviderEvaluation.<String>builder()
130+
.flagMetadata(ImmutableMetadata.builder()
131+
.addString("contextId", "realContextId")
132+
.addString("flagSetId", "realFlagSetId")
133+
.addString("version", "realVersion")
134+
.build())
135+
.reason(Reason.DEFAULT.name())
136+
.variant("realVariant")
137+
.build();
138+
139+
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation);
140+
141+
assertEquals("realFlag", event.getAttributes().get(Telemetry.TELEMETRY_KEY));
142+
assertEquals("realProviderName", event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER));
143+
assertEquals("default", event.getAttributes().get(Telemetry.TELEMETRY_REASON));
144+
assertEquals("realContextId", event.getAttributes().get(Telemetry.TELEMETRY_CONTEXT_ID));
145+
assertEquals("realFlagSetId", event.getAttributes().get(Telemetry.TELEMETRY_FLAG_SET_ID));
146+
assertEquals("realVersion", event.getAttributes().get(Telemetry.TELEMETRY_VERSION));
147+
assertNull(event.getAttributes().get(Telemetry.TELEMETRY_ERROR_CODE));
148+
assertEquals("realVariant", event.getAttributes().get(Telemetry.TELEMETRY_VARIANT));
149+
150+
}
151+
152+
@Test
153+
public void testErrorEvaluation() {
154+
EvaluationContext evaluationContext = mock(EvaluationContext.class);
155+
when(evaluationContext.getTargetingKey()).thenReturn("realTargetingKey");
156+
157+
Metadata providerMetadata = mock(Metadata.class);
158+
when(providerMetadata.getName()).thenReturn("realProviderName");
159+
160+
HookContext<String> hookContext = HookContext.<String>builder()
161+
.flagKey("realFlag")
162+
.type(FlagValueType.STRING)
163+
.defaultValue("realDefault")
164+
.ctx(evaluationContext)
165+
.clientMetadata(mock(ClientMetadata.class))
166+
.providerMetadata(providerMetadata)
167+
.build();
168+
169+
ProviderEvaluation<String> providerEvaluation = ProviderEvaluation.<String>builder()
170+
.flagMetadata(ImmutableMetadata.builder()
171+
.addString("contextId", "realContextId")
172+
.addString("flagSetId", "realFlagSetId")
173+
.addString("version", "realVersion")
174+
.build())
175+
.reason(Reason.ERROR.name())
176+
.errorMessage("realErrorMessage")
177+
.build();
178+
179+
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation);
180+
181+
assertEquals("realFlag", event.getAttributes().get(Telemetry.TELEMETRY_KEY));
182+
assertEquals("realProviderName", event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER));
183+
assertEquals("error", event.getAttributes().get(Telemetry.TELEMETRY_REASON));
184+
assertEquals("realContextId", event.getAttributes().get(Telemetry.TELEMETRY_CONTEXT_ID));
185+
assertEquals("realFlagSetId", event.getAttributes().get(Telemetry.TELEMETRY_FLAG_SET_ID));
186+
assertEquals("realVersion", event.getAttributes().get(Telemetry.TELEMETRY_VERSION));
187+
assertEquals(ErrorCode.GENERAL, event.getAttributes().get(Telemetry.TELEMETRY_ERROR_CODE));
188+
assertEquals("realErrorMessage", event.getAttributes().get(Telemetry.TELEMETRY_ERROR_MSG));
189+
assertNull(event.getAttributes().get(Telemetry.TELEMETRY_VARIANT));
190+
191+
}
192+
193+
@Test
194+
public void testErrorCodeEvaluation() {
195+
EvaluationContext evaluationContext = mock(EvaluationContext.class);
196+
when(evaluationContext.getTargetingKey()).thenReturn("realTargetingKey");
197+
198+
Metadata providerMetadata = mock(Metadata.class);
199+
when(providerMetadata.getName()).thenReturn("realProviderName");
200+
201+
HookContext<String> hookContext = HookContext.<String>builder()
202+
.flagKey("realFlag")
203+
.type(FlagValueType.STRING)
204+
.defaultValue("realDefault")
205+
.ctx(evaluationContext)
206+
.clientMetadata(mock(ClientMetadata.class))
207+
.providerMetadata(providerMetadata)
208+
.build();
209+
210+
ProviderEvaluation<String> providerEvaluation = ProviderEvaluation.<String>builder()
211+
.flagMetadata(ImmutableMetadata.builder()
212+
.addString("contextId", "realContextId")
213+
.addString("flagSetId", "realFlagSetId")
214+
.addString("version", "realVersion")
215+
.build())
216+
.reason(Reason.ERROR.name())
217+
.errorMessage("realErrorMessage")
218+
.errorCode(ErrorCode.INVALID_CONTEXT)
219+
.build();
220+
221+
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation);
222+
223+
assertEquals("realFlag", event.getAttributes().get(Telemetry.TELEMETRY_KEY));
224+
assertEquals("realProviderName", event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER));
225+
assertEquals("error", event.getAttributes().get(Telemetry.TELEMETRY_REASON));
226+
assertEquals("realContextId", event.getAttributes().get(Telemetry.TELEMETRY_CONTEXT_ID));
227+
assertEquals("realFlagSetId", event.getAttributes().get(Telemetry.TELEMETRY_FLAG_SET_ID));
228+
assertEquals("realVersion", event.getAttributes().get(Telemetry.TELEMETRY_VERSION));
229+
assertEquals(ErrorCode.INVALID_CONTEXT, event.getAttributes().get(Telemetry.TELEMETRY_ERROR_CODE));
230+
assertEquals("realErrorMessage", event.getAttributes().get(Telemetry.TELEMETRY_ERROR_MSG));
231+
assertNull(event.getAttributes().get(Telemetry.TELEMETRY_VARIANT));
232+
233+
}
234+
235+
}

0 commit comments

Comments
 (0)