-
Notifications
You must be signed in to change notification settings - Fork 48
feat: add telemetry helper utils #1346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
5349db8
feat: add telemetry helper utils
liran2000 1a33af2
updates
liran2000 6c67511
Merge branch 'main' into issue/1327
liran2000 2d2104b
fixup: apply changes according to the semconv
aepfli fe19ca0
fixup: fix tests
aepfli 9d78142
fixup: fix spotless
aepfli dbc41b7
Merge branch 'main' into issue/1327
aepfli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package dev.openfeature.sdk; | ||
|
||
import java.util.HashMap; | ||
import java.util.Map; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.Singular; | ||
|
||
/** | ||
* Represents an evaluation event. | ||
*/ | ||
@Builder | ||
@Getter | ||
public class EvaluationEvent { | ||
|
||
private String name; | ||
|
||
@Singular("attribute") | ||
private Map<String, Object> attributes; | ||
|
||
@Singular("bodyElement") | ||
private Map<String, Object> body; | ||
|
||
public Map<String, Object> getAttributes() { | ||
return new HashMap<>(attributes); | ||
} | ||
|
||
public Map<String, Object> getBody() { | ||
return new HashMap<>(body); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package dev.openfeature.sdk; | ||
|
||
/** | ||
* The Telemetry class provides constants and methods for creating OpenTelemetry compliant | ||
* evaluation events. | ||
*/ | ||
public class Telemetry { | ||
|
||
private Telemetry() {} | ||
|
||
/* | ||
The OpenTelemetry compliant event attributes for flag evaluation. | ||
Specification: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/ | ||
*/ | ||
public static final String TELEMETRY_KEY = "feature_flag.key"; | ||
public static final String TELEMETRY_ERROR_CODE = "error.type"; | ||
public static final String TELEMETRY_VARIANT = "feature_flag.variant"; | ||
public static final String TELEMETRY_CONTEXT_ID = "feature_flag.context.id"; | ||
public static final String TELEMETRY_ERROR_MSG = "feature_flag.evaluation.error.message"; | ||
public static final String TELEMETRY_REASON = "feature_flag.evaluation.reason"; | ||
public static final String TELEMETRY_PROVIDER = "feature_flag.provider_name"; | ||
public static final String TELEMETRY_FLAG_SET_ID = "feature_flag.set.id"; | ||
public static final String TELEMETRY_VERSION = "feature_flag.version"; | ||
|
||
// Well-known flag metadata attributes for telemetry events. | ||
// Specification: https://openfeature.dev/specification/appendix-d#flag-metadata | ||
public static final String TELEMETRY_FLAG_META_CONTEXT_ID = "contextId"; | ||
public static final String TELEMETRY_FLAG_META_FLAG_SET_ID = "flagSetId"; | ||
public static final String TELEMETRY_FLAG_META_VERSION = "version"; | ||
|
||
// OpenTelemetry event body. | ||
// Specification: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/ | ||
public static final String TELEMETRY_BODY = "value"; | ||
|
||
public static final String FLAG_EVALUATION_EVENT_NAME = "feature_flag.evaluation"; | ||
|
||
/** | ||
* Creates an EvaluationEvent using the provided HookContext and ProviderEvaluation. | ||
* | ||
* @param hookContext the context containing flag evaluation details | ||
* @param providerEvaluation the evaluation result from the provider | ||
* | ||
* @return an EvaluationEvent populated with telemetry data | ||
*/ | ||
public static EvaluationEvent createEvaluationEvent( | ||
HookContext<?> hookContext, ProviderEvaluation<?> providerEvaluation) { | ||
EvaluationEvent.EvaluationEventBuilder evaluationEventBuilder = EvaluationEvent.builder() | ||
.name(FLAG_EVALUATION_EVENT_NAME) | ||
.attribute(TELEMETRY_KEY, hookContext.getFlagKey()) | ||
.attribute(TELEMETRY_PROVIDER, hookContext.getProviderMetadata().getName()); | ||
|
||
if (providerEvaluation.getReason() != null) { | ||
evaluationEventBuilder.attribute( | ||
TELEMETRY_REASON, providerEvaluation.getReason().toLowerCase()); | ||
} else { | ||
evaluationEventBuilder.attribute( | ||
TELEMETRY_REASON, Reason.UNKNOWN.name().toLowerCase()); | ||
} | ||
|
||
if (providerEvaluation.getVariant() != null) { | ||
evaluationEventBuilder.attribute(TELEMETRY_VARIANT, providerEvaluation.getVariant()); | ||
} else { | ||
evaluationEventBuilder.bodyElement(TELEMETRY_BODY, providerEvaluation.getValue()); | ||
} | ||
|
||
String contextId = providerEvaluation.getFlagMetadata().getString(TELEMETRY_FLAG_META_CONTEXT_ID); | ||
if (contextId != null) { | ||
evaluationEventBuilder.attribute(TELEMETRY_CONTEXT_ID, contextId); | ||
} else { | ||
evaluationEventBuilder.attribute( | ||
TELEMETRY_CONTEXT_ID, hookContext.getCtx().getTargetingKey()); | ||
} | ||
|
||
String setID = providerEvaluation.getFlagMetadata().getString(TELEMETRY_FLAG_META_FLAG_SET_ID); | ||
if (setID != null) { | ||
evaluationEventBuilder.attribute(TELEMETRY_FLAG_SET_ID, setID); | ||
} | ||
|
||
String version = providerEvaluation.getFlagMetadata().getString(TELEMETRY_FLAG_META_VERSION); | ||
if (version != null) { | ||
evaluationEventBuilder.attribute(TELEMETRY_VERSION, version); | ||
} | ||
|
||
if (Reason.ERROR.name().equals(providerEvaluation.getReason())) { | ||
if (providerEvaluation.getErrorCode() != null) { | ||
evaluationEventBuilder.attribute(TELEMETRY_ERROR_CODE, providerEvaluation.getErrorCode()); | ||
} else { | ||
evaluationEventBuilder.attribute(TELEMETRY_ERROR_CODE, ErrorCode.GENERAL); | ||
} | ||
|
||
if (providerEvaluation.getErrorMessage() != null) { | ||
evaluationEventBuilder.attribute(TELEMETRY_ERROR_MSG, providerEvaluation.getErrorMessage()); | ||
} | ||
} | ||
|
||
return evaluationEventBuilder.build(); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
package dev.openfeature.sdk; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertNull; | ||
import static org.mockito.Mockito.mock; | ||
import static org.mockito.Mockito.when; | ||
|
||
import org.junit.jupiter.api.Test; | ||
|
||
public class TelemetryTest { | ||
|
||
@Test | ||
void testCreatesEvaluationEventWithMandatoryFields() { | ||
// Arrange | ||
String flagKey = "test-flag"; | ||
String providerName = "test-provider"; | ||
String reason = "static"; | ||
|
||
Metadata providerMetadata = mock(Metadata.class); | ||
when(providerMetadata.getName()).thenReturn(providerName); | ||
|
||
HookContext<Boolean> hookContext = HookContext.<Boolean>builder() | ||
.flagKey(flagKey) | ||
.providerMetadata(providerMetadata) | ||
.type(FlagValueType.BOOLEAN) | ||
.defaultValue(false) | ||
.ctx(new ImmutableContext()) | ||
.build(); | ||
|
||
ProviderEvaluation<Boolean> evaluation = | ||
ProviderEvaluation.<Boolean>builder().reason(reason).value(true).build(); | ||
|
||
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, evaluation); | ||
|
||
assertEquals(Telemetry.FLAG_EVALUATION_EVENT_NAME, event.getName()); | ||
assertEquals(flagKey, event.getAttributes().get(Telemetry.TELEMETRY_KEY)); | ||
assertEquals(providerName, event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER)); | ||
assertEquals(reason.toLowerCase(), event.getAttributes().get(Telemetry.TELEMETRY_REASON)); | ||
} | ||
|
||
@Test | ||
void testHandlesNullReason() { | ||
// Arrange | ||
String flagKey = "test-flag"; | ||
String providerName = "test-provider"; | ||
|
||
Metadata providerMetadata = mock(Metadata.class); | ||
when(providerMetadata.getName()).thenReturn(providerName); | ||
|
||
HookContext<Boolean> hookContext = HookContext.<Boolean>builder() | ||
.flagKey(flagKey) | ||
.providerMetadata(providerMetadata) | ||
.type(FlagValueType.BOOLEAN) | ||
.defaultValue(false) | ||
.ctx(new ImmutableContext()) | ||
.build(); | ||
|
||
ProviderEvaluation<Boolean> evaluation = | ||
ProviderEvaluation.<Boolean>builder().reason(null).value(true).build(); | ||
|
||
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, evaluation); | ||
|
||
assertEquals(Reason.UNKNOWN.name().toLowerCase(), event.getAttributes().get(Telemetry.TELEMETRY_REASON)); | ||
} | ||
|
||
@Test | ||
void testSetsVariantAttributeWhenVariantExists() { | ||
HookContext<String> hookContext = HookContext.<String>builder() | ||
.flagKey("testFlag") | ||
.type(FlagValueType.STRING) | ||
.defaultValue("default") | ||
.ctx(mock(EvaluationContext.class)) | ||
.clientMetadata(mock(ClientMetadata.class)) | ||
.providerMetadata(mock(Metadata.class)) | ||
.build(); | ||
|
||
ProviderEvaluation<String> providerEvaluation = ProviderEvaluation.<String>builder() | ||
.variant("testVariant") | ||
.flagMetadata(ImmutableMetadata.builder().build()) | ||
.build(); | ||
|
||
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation); | ||
|
||
assertEquals("testVariant", event.getAttributes().get(Telemetry.TELEMETRY_VARIANT)); | ||
} | ||
|
||
@Test | ||
void test_sets_value_in_body_when_variant_is_null() { | ||
HookContext<String> hookContext = HookContext.<String>builder() | ||
.flagKey("testFlag") | ||
.type(FlagValueType.STRING) | ||
.defaultValue("default") | ||
.ctx(mock(EvaluationContext.class)) | ||
.clientMetadata(mock(ClientMetadata.class)) | ||
.providerMetadata(mock(Metadata.class)) | ||
.build(); | ||
|
||
ProviderEvaluation<String> providerEvaluation = ProviderEvaluation.<String>builder() | ||
.value("testValue") | ||
.flagMetadata(ImmutableMetadata.builder().build()) | ||
.build(); | ||
|
||
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation); | ||
|
||
assertEquals("testValue", event.getBody().get(Telemetry.TELEMETRY_BODY)); | ||
} | ||
|
||
@Test | ||
void testAllFieldsPopulated() { | ||
EvaluationContext evaluationContext = mock(EvaluationContext.class); | ||
when(evaluationContext.getTargetingKey()).thenReturn("realTargetingKey"); | ||
|
||
Metadata providerMetadata = mock(Metadata.class); | ||
when(providerMetadata.getName()).thenReturn("realProviderName"); | ||
|
||
HookContext<String> hookContext = HookContext.<String>builder() | ||
.flagKey("realFlag") | ||
.type(FlagValueType.STRING) | ||
.defaultValue("realDefault") | ||
.ctx(evaluationContext) | ||
.clientMetadata(mock(ClientMetadata.class)) | ||
.providerMetadata(providerMetadata) | ||
.build(); | ||
|
||
ProviderEvaluation<String> providerEvaluation = ProviderEvaluation.<String>builder() | ||
.flagMetadata(ImmutableMetadata.builder() | ||
.addString("contextId", "realContextId") | ||
.addString("flagSetId", "realFlagSetId") | ||
.addString("version", "realVersion") | ||
.build()) | ||
.reason(Reason.DEFAULT.name()) | ||
.variant("realVariant") | ||
.build(); | ||
|
||
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation); | ||
|
||
assertEquals("realFlag", event.getAttributes().get(Telemetry.TELEMETRY_KEY)); | ||
assertEquals("realProviderName", event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER)); | ||
assertEquals("default", event.getAttributes().get(Telemetry.TELEMETRY_REASON)); | ||
assertEquals("realContextId", event.getAttributes().get(Telemetry.TELEMETRY_CONTEXT_ID)); | ||
assertEquals("realFlagSetId", event.getAttributes().get(Telemetry.TELEMETRY_FLAG_SET_ID)); | ||
assertEquals("realVersion", event.getAttributes().get(Telemetry.TELEMETRY_VERSION)); | ||
assertNull(event.getAttributes().get(Telemetry.TELEMETRY_ERROR_CODE)); | ||
assertEquals("realVariant", event.getAttributes().get(Telemetry.TELEMETRY_VARIANT)); | ||
} | ||
|
||
@Test | ||
void testErrorEvaluation() { | ||
EvaluationContext evaluationContext = mock(EvaluationContext.class); | ||
when(evaluationContext.getTargetingKey()).thenReturn("realTargetingKey"); | ||
|
||
Metadata providerMetadata = mock(Metadata.class); | ||
when(providerMetadata.getName()).thenReturn("realProviderName"); | ||
|
||
HookContext<String> hookContext = HookContext.<String>builder() | ||
.flagKey("realFlag") | ||
.type(FlagValueType.STRING) | ||
.defaultValue("realDefault") | ||
.ctx(evaluationContext) | ||
.clientMetadata(mock(ClientMetadata.class)) | ||
.providerMetadata(providerMetadata) | ||
.build(); | ||
|
||
ProviderEvaluation<String> providerEvaluation = ProviderEvaluation.<String>builder() | ||
.flagMetadata(ImmutableMetadata.builder() | ||
.addString("contextId", "realContextId") | ||
.addString("flagSetId", "realFlagSetId") | ||
.addString("version", "realVersion") | ||
.build()) | ||
.reason(Reason.ERROR.name()) | ||
.errorMessage("realErrorMessage") | ||
.build(); | ||
|
||
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation); | ||
|
||
assertEquals("realFlag", event.getAttributes().get(Telemetry.TELEMETRY_KEY)); | ||
assertEquals("realProviderName", event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER)); | ||
assertEquals("error", event.getAttributes().get(Telemetry.TELEMETRY_REASON)); | ||
assertEquals("realContextId", event.getAttributes().get(Telemetry.TELEMETRY_CONTEXT_ID)); | ||
assertEquals("realFlagSetId", event.getAttributes().get(Telemetry.TELEMETRY_FLAG_SET_ID)); | ||
assertEquals("realVersion", event.getAttributes().get(Telemetry.TELEMETRY_VERSION)); | ||
assertEquals(ErrorCode.GENERAL, event.getAttributes().get(Telemetry.TELEMETRY_ERROR_CODE)); | ||
assertEquals("realErrorMessage", event.getAttributes().get(Telemetry.TELEMETRY_ERROR_MSG)); | ||
assertNull(event.getAttributes().get(Telemetry.TELEMETRY_VARIANT)); | ||
} | ||
|
||
@Test | ||
void testErrorCodeEvaluation() { | ||
EvaluationContext evaluationContext = mock(EvaluationContext.class); | ||
when(evaluationContext.getTargetingKey()).thenReturn("realTargetingKey"); | ||
|
||
Metadata providerMetadata = mock(Metadata.class); | ||
when(providerMetadata.getName()).thenReturn("realProviderName"); | ||
|
||
HookContext<String> hookContext = HookContext.<String>builder() | ||
.flagKey("realFlag") | ||
.type(FlagValueType.STRING) | ||
.defaultValue("realDefault") | ||
.ctx(evaluationContext) | ||
.clientMetadata(mock(ClientMetadata.class)) | ||
.providerMetadata(providerMetadata) | ||
.build(); | ||
|
||
ProviderEvaluation<String> providerEvaluation = ProviderEvaluation.<String>builder() | ||
.flagMetadata(ImmutableMetadata.builder() | ||
.addString("contextId", "realContextId") | ||
.addString("flagSetId", "realFlagSetId") | ||
.addString("version", "realVersion") | ||
.build()) | ||
.reason(Reason.ERROR.name()) | ||
.errorMessage("realErrorMessage") | ||
.errorCode(ErrorCode.INVALID_CONTEXT) | ||
.build(); | ||
|
||
EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation); | ||
|
||
assertEquals("realFlag", event.getAttributes().get(Telemetry.TELEMETRY_KEY)); | ||
assertEquals("realProviderName", event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER)); | ||
assertEquals("error", event.getAttributes().get(Telemetry.TELEMETRY_REASON)); | ||
assertEquals("realContextId", event.getAttributes().get(Telemetry.TELEMETRY_CONTEXT_ID)); | ||
assertEquals("realFlagSetId", event.getAttributes().get(Telemetry.TELEMETRY_FLAG_SET_ID)); | ||
assertEquals("realVersion", event.getAttributes().get(Telemetry.TELEMETRY_VERSION)); | ||
assertEquals(ErrorCode.INVALID_CONTEXT, event.getAttributes().get(Telemetry.TELEMETRY_ERROR_CODE)); | ||
assertEquals("realErrorMessage", event.getAttributes().get(Telemetry.TELEMETRY_ERROR_MSG)); | ||
assertNull(event.getAttributes().get(Telemetry.TELEMETRY_VARIANT)); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we want to use
FlagEvaluationDetails
here instead of theProviderEvaluation
, as pointed out here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good catch. The
finally
hook was updated in this PR.