-
Notifications
You must be signed in to change notification settings - Fork 42
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
feat: add telemetry helper utils #1346
Open
liran2000
wants to merge
3
commits into
open-feature:main
Choose a base branch
from
liran2000:issue/1327
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
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 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 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 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.