Skip to content

Commit d0ae548

Browse files
liran2000aepfli
andauthored
feat: add telemetry helper utils (#1346)
* feat: add telemetry helper utils Signed-off-by: liran2000 <[email protected]> * updates Signed-off-by: liran2000 <[email protected]> * fixup: apply changes according to the semconv The semconv has changed, and some attributes have been renamed. Furthermore, the body usage is deprecated and should be part of the attributes. see: open-telemetry/semantic-conventions#1990 Signed-off-by: Simon Schrottner <[email protected]> * fixup: fix tests Signed-off-by: Simon Schrottner <[email protected]> * fixup: fix spotless Signed-off-by: Simon Schrottner <[email protected]> --------- Signed-off-by: liran2000 <[email protected]> Signed-off-by: Simon Schrottner <[email protected]> Co-authored-by: Simon Schrottner <[email protected]>
1 parent e568f3a commit d0ae548

File tree

3 files changed

+350
-0
lines changed

3 files changed

+350
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
public Map<String, Object> getAttributes() {
22+
return new HashMap<>(attributes);
23+
}
24+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
private Telemetry() {}
10+
11+
/*
12+
The OpenTelemetry compliant event attributes for flag evaluation.
13+
Specification: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
14+
*/
15+
public static final String TELEMETRY_KEY = "feature_flag.key";
16+
public static final String TELEMETRY_ERROR_CODE = "error.type";
17+
public static final String TELEMETRY_VARIANT = "feature_flag.result.variant";
18+
public static final String TELEMETRY_VALUE = "feature_flag.result.value";
19+
public static final String TELEMETRY_CONTEXT_ID = "feature_flag.context.id";
20+
public static final String TELEMETRY_ERROR_MSG = "feature_flag.evaluation.error.message";
21+
public static final String TELEMETRY_REASON = "feature_flag.result.reason";
22+
public static final String TELEMETRY_PROVIDER = "feature_flag.provider.name";
23+
public static final String TELEMETRY_FLAG_SET_ID = "feature_flag.set.id";
24+
public static final String TELEMETRY_VERSION = "feature_flag.version";
25+
26+
// Well-known flag metadata attributes for telemetry events.
27+
// Specification: https://openfeature.dev/specification/appendix-d#flag-metadata
28+
public static final String TELEMETRY_FLAG_META_CONTEXT_ID = "contextId";
29+
public static final String TELEMETRY_FLAG_META_FLAG_SET_ID = "flagSetId";
30+
public static final String TELEMETRY_FLAG_META_VERSION = "version";
31+
32+
public static final String FLAG_EVALUATION_EVENT_NAME = "feature_flag.evaluation";
33+
34+
/**
35+
* Creates an EvaluationEvent using the provided HookContext and ProviderEvaluation.
36+
*
37+
* @param hookContext the context containing flag evaluation details
38+
* @param evaluationDetails the evaluation result from the provider
39+
*
40+
* @return an EvaluationEvent populated with telemetry data
41+
*/
42+
public static EvaluationEvent createEvaluationEvent(
43+
HookContext<?> hookContext, FlagEvaluationDetails<?> evaluationDetails) {
44+
EvaluationEvent.EvaluationEventBuilder evaluationEventBuilder = EvaluationEvent.builder()
45+
.name(FLAG_EVALUATION_EVENT_NAME)
46+
.attribute(TELEMETRY_KEY, hookContext.getFlagKey())
47+
.attribute(TELEMETRY_PROVIDER, hookContext.getProviderMetadata().getName());
48+
49+
if (evaluationDetails.getReason() != null) {
50+
evaluationEventBuilder.attribute(
51+
TELEMETRY_REASON, evaluationDetails.getReason().toLowerCase());
52+
} else {
53+
evaluationEventBuilder.attribute(
54+
TELEMETRY_REASON, Reason.UNKNOWN.name().toLowerCase());
55+
}
56+
57+
if (evaluationDetails.getVariant() != null) {
58+
evaluationEventBuilder.attribute(TELEMETRY_VARIANT, evaluationDetails.getVariant());
59+
} else {
60+
evaluationEventBuilder.attribute(TELEMETRY_VALUE, evaluationDetails.getValue());
61+
}
62+
63+
String contextId = evaluationDetails.getFlagMetadata().getString(TELEMETRY_FLAG_META_CONTEXT_ID);
64+
if (contextId != null) {
65+
evaluationEventBuilder.attribute(TELEMETRY_CONTEXT_ID, contextId);
66+
} else {
67+
evaluationEventBuilder.attribute(
68+
TELEMETRY_CONTEXT_ID, hookContext.getCtx().getTargetingKey());
69+
}
70+
71+
String setID = evaluationDetails.getFlagMetadata().getString(TELEMETRY_FLAG_META_FLAG_SET_ID);
72+
if (setID != null) {
73+
evaluationEventBuilder.attribute(TELEMETRY_FLAG_SET_ID, setID);
74+
}
75+
76+
String version = evaluationDetails.getFlagMetadata().getString(TELEMETRY_FLAG_META_VERSION);
77+
if (version != null) {
78+
evaluationEventBuilder.attribute(TELEMETRY_VERSION, version);
79+
}
80+
81+
if (Reason.ERROR.name().equals(evaluationDetails.getReason())) {
82+
if (evaluationDetails.getErrorCode() != null) {
83+
evaluationEventBuilder.attribute(TELEMETRY_ERROR_CODE, evaluationDetails.getErrorCode());
84+
} else {
85+
evaluationEventBuilder.attribute(TELEMETRY_ERROR_CODE, ErrorCode.GENERAL);
86+
}
87+
88+
if (evaluationDetails.getErrorMessage() != null) {
89+
evaluationEventBuilder.attribute(TELEMETRY_ERROR_MSG, evaluationDetails.getErrorMessage());
90+
}
91+
}
92+
93+
return evaluationEventBuilder.build();
94+
}
95+
}
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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+
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+
FlagEvaluationDetails<Boolean> evaluation = FlagEvaluationDetails.<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+
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+
FlagEvaluationDetails<Boolean> evaluation = FlagEvaluationDetails.<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+
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+
FlagEvaluationDetails<String> providerEvaluation = FlagEvaluationDetails.<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+
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+
FlagEvaluationDetails<String> providerEvaluation = FlagEvaluationDetails.<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.getAttributes().get(Telemetry.TELEMETRY_VALUE));
110+
}
111+
112+
@Test
113+
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+
FlagEvaluationDetails<String> providerEvaluation = FlagEvaluationDetails.<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+
@Test
152+
void testErrorEvaluation() {
153+
EvaluationContext evaluationContext = mock(EvaluationContext.class);
154+
when(evaluationContext.getTargetingKey()).thenReturn("realTargetingKey");
155+
156+
Metadata providerMetadata = mock(Metadata.class);
157+
when(providerMetadata.getName()).thenReturn("realProviderName");
158+
159+
HookContext<String> hookContext = HookContext.<String>builder()
160+
.flagKey("realFlag")
161+
.type(FlagValueType.STRING)
162+
.defaultValue("realDefault")
163+
.ctx(evaluationContext)
164+
.clientMetadata(mock(ClientMetadata.class))
165+
.providerMetadata(providerMetadata)
166+
.build();
167+
168+
FlagEvaluationDetails<String> providerEvaluation = FlagEvaluationDetails.<String>builder()
169+
.flagMetadata(ImmutableMetadata.builder()
170+
.addString("contextId", "realContextId")
171+
.addString("flagSetId", "realFlagSetId")
172+
.addString("version", "realVersion")
173+
.build())
174+
.reason(Reason.ERROR.name())
175+
.errorMessage("realErrorMessage")
176+
.build();
177+
178+
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation);
179+
180+
assertEquals("realFlag", event.getAttributes().get(Telemetry.TELEMETRY_KEY));
181+
assertEquals("realProviderName", event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER));
182+
assertEquals("error", event.getAttributes().get(Telemetry.TELEMETRY_REASON));
183+
assertEquals("realContextId", event.getAttributes().get(Telemetry.TELEMETRY_CONTEXT_ID));
184+
assertEquals("realFlagSetId", event.getAttributes().get(Telemetry.TELEMETRY_FLAG_SET_ID));
185+
assertEquals("realVersion", event.getAttributes().get(Telemetry.TELEMETRY_VERSION));
186+
assertEquals(ErrorCode.GENERAL, event.getAttributes().get(Telemetry.TELEMETRY_ERROR_CODE));
187+
assertEquals("realErrorMessage", event.getAttributes().get(Telemetry.TELEMETRY_ERROR_MSG));
188+
assertNull(event.getAttributes().get(Telemetry.TELEMETRY_VARIANT));
189+
}
190+
191+
@Test
192+
void testErrorCodeEvaluation() {
193+
EvaluationContext evaluationContext = mock(EvaluationContext.class);
194+
when(evaluationContext.getTargetingKey()).thenReturn("realTargetingKey");
195+
196+
Metadata providerMetadata = mock(Metadata.class);
197+
when(providerMetadata.getName()).thenReturn("realProviderName");
198+
199+
HookContext<String> hookContext = HookContext.<String>builder()
200+
.flagKey("realFlag")
201+
.type(FlagValueType.STRING)
202+
.defaultValue("realDefault")
203+
.ctx(evaluationContext)
204+
.clientMetadata(mock(ClientMetadata.class))
205+
.providerMetadata(providerMetadata)
206+
.build();
207+
208+
FlagEvaluationDetails<String> providerEvaluation = FlagEvaluationDetails.<String>builder()
209+
.flagMetadata(ImmutableMetadata.builder()
210+
.addString("contextId", "realContextId")
211+
.addString("flagSetId", "realFlagSetId")
212+
.addString("version", "realVersion")
213+
.build())
214+
.reason(Reason.ERROR.name())
215+
.errorMessage("realErrorMessage")
216+
.errorCode(ErrorCode.INVALID_CONTEXT)
217+
.build();
218+
219+
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation);
220+
221+
assertEquals("realFlag", event.getAttributes().get(Telemetry.TELEMETRY_KEY));
222+
assertEquals("realProviderName", event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER));
223+
assertEquals("error", event.getAttributes().get(Telemetry.TELEMETRY_REASON));
224+
assertEquals("realContextId", event.getAttributes().get(Telemetry.TELEMETRY_CONTEXT_ID));
225+
assertEquals("realFlagSetId", event.getAttributes().get(Telemetry.TELEMETRY_FLAG_SET_ID));
226+
assertEquals("realVersion", event.getAttributes().get(Telemetry.TELEMETRY_VERSION));
227+
assertEquals(ErrorCode.INVALID_CONTEXT, event.getAttributes().get(Telemetry.TELEMETRY_ERROR_CODE));
228+
assertEquals("realErrorMessage", event.getAttributes().get(Telemetry.TELEMETRY_ERROR_MSG));
229+
assertNull(event.getAttributes().get(Telemetry.TELEMETRY_VARIANT));
230+
}
231+
}

0 commit comments

Comments
 (0)