Skip to content

feat: add otel compatible telemetry signal #129

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 9 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions android/api/android.api
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ public abstract interface class dev/openfeature/sdk/EvaluationContext : dev/open
public abstract fun withTargetingKey (Ljava/lang/String;)Ldev/openfeature/sdk/EvaluationContext;
}

public final class dev/openfeature/sdk/EvaluationEvent {
public fun <init> (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/util/Map;
public final fun component3 ()Ljava/util/Map;
public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)Ldev/openfeature/sdk/EvaluationEvent;
public static synthetic fun copy$default (Ldev/openfeature/sdk/EvaluationEvent;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Ldev/openfeature/sdk/EvaluationEvent;
public fun equals (Ljava/lang/Object;)Z
public final fun getAttributes ()Ljava/util/Map;
public final fun getBody ()Ljava/util/Map;
public final fun getName ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class dev/openfeature/sdk/EvaluationMetadata {
public static final field Companion Ldev/openfeature/sdk/EvaluationMetadata$Companion;
public fun equals (Ljava/lang/Object;)Z
Expand Down Expand Up @@ -398,6 +413,24 @@ public abstract interface class dev/openfeature/sdk/Structure {
public abstract fun keySet ()Ljava/util/Set;
}

public final class dev/openfeature/sdk/TelemetryKt {
public static final field FLAG_EVALUATION_EVENT_NAME Ljava/lang/String;
public static final field TELEMETRY_BODY Ljava/lang/String;
public static final field TELEMETRY_CONTEXT_ID Ljava/lang/String;
public static final field TELEMETRY_ERROR_CODE Ljava/lang/String;
public static final field TELEMETRY_ERROR_MSG Ljava/lang/String;
public static final field TELEMETRY_FLAG_META_CONTEXT_ID Ljava/lang/String;
public static final field TELEMETRY_FLAG_META_FLAG_SET_ID Ljava/lang/String;
public static final field TELEMETRY_FLAG_META_VERSION Ljava/lang/String;
public static final field TELEMETRY_FLAG_SET_ID Ljava/lang/String;
public static final field TELEMETRY_KEY Ljava/lang/String;
public static final field TELEMETRY_PROVIDER Ljava/lang/String;
public static final field TELEMETRY_REASON Ljava/lang/String;
public static final field TELEMETRY_VARIANT Ljava/lang/String;
public static final field TELEMETRY_VERSION Ljava/lang/String;
public static final fun createEvaluationEvent (Ldev/openfeature/sdk/HookContext;Ldev/openfeature/sdk/FlagEvaluationDetails;)Ldev/openfeature/sdk/EvaluationEvent;
}

public abstract interface class dev/openfeature/sdk/Tracking {
public abstract fun track (Ljava/lang/String;Ldev/openfeature/sdk/TrackingEventDetails;)V
}
Expand Down
27 changes: 27 additions & 0 deletions android/src/main/java/dev/openfeature/sdk/EvaluationEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dev.openfeature.sdk

/**
* OpenTelemetry compatible telemetry signal for flag evaluations. Can be created by calling [createEvaluationEvent].
*
* See
* [TELEMETRY_KEY],
* [TELEMETRY_ERROR_CODE],
* [TELEMETRY_VARIANT],
* [TELEMETRY_CONTEXT_ID],
* [TELEMETRY_ERROR_MSG],
* [TELEMETRY_REASON],
* [TELEMETRY_PROVIDER],
* [TELEMETRY_FLAG_SET_ID],
* [TELEMETRY_VERSION],
* [TELEMETRY_FLAG_META_CONTEXT_ID],
* [TELEMETRY_FLAG_META_FLAG_SET_ID],
* [TELEMETRY_FLAG_META_VERSION],
* [TELEMETRY_BODY] and
* [FLAG_EVALUATION_EVENT_NAME]
* for attribute and body keys.
*/
data class EvaluationEvent(
val name: String,
val attributes: Map<String, Any?>,
val body: Map<String, Any?>
)
64 changes: 64 additions & 0 deletions android/src/main/java/dev/openfeature/sdk/Telemetry.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package dev.openfeature.sdk

import dev.openfeature.sdk.exceptions.ErrorCode

const val TELEMETRY_KEY = "feature_flag.key"
const val TELEMETRY_ERROR_CODE = "error.type"
const val TELEMETRY_VARIANT = "feature_flag.variant"
const val TELEMETRY_CONTEXT_ID = "feature_flag.context.id"
const val TELEMETRY_ERROR_MSG = "feature_flag.evaluation.error.message"
const val TELEMETRY_REASON = "feature_flag.evaluation.reason"
const val TELEMETRY_PROVIDER = "feature_flag.provider_name"
const val TELEMETRY_FLAG_SET_ID = "feature_flag.set.id"
const val TELEMETRY_VERSION = "feature_flag.version"

// Well-known flag metadata attributes for telemetry events.
// Specification: https://openfeature.dev/specification/appendix-d#flag-metadata
const val TELEMETRY_FLAG_META_CONTEXT_ID = "contextId"
const val TELEMETRY_FLAG_META_FLAG_SET_ID = "flagSetId"
const val TELEMETRY_FLAG_META_VERSION = "version"

// OpenTelemetry event body.
// Specification: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
const val TELEMETRY_BODY = "value"

const val FLAG_EVALUATION_EVENT_NAME = "feature_flag.evaluation"

/**
* Creates an [EvaluationEvent] from a flag evaluation. To be used inside a finally hook to provide an OpenTelemetry
* compatible telemetry signal.
*/
fun <T> createEvaluationEvent(
hookContext: HookContext<T>,
flagEvaluationDetails: FlagEvaluationDetails<T>
): EvaluationEvent {
val attributes = mutableMapOf<String, Any?>()
val body = mutableMapOf<String, Any?>()
attributes[TELEMETRY_KEY] = hookContext.flagKey
attributes[TELEMETRY_PROVIDER] = hookContext.providerMetadata.name ?: ""
attributes[TELEMETRY_REASON] = flagEvaluationDetails.reason?.lowercase() ?: Reason.UNKNOWN.name.lowercase()
attributes[TELEMETRY_CONTEXT_ID] =
flagEvaluationDetails.metadata.getString(TELEMETRY_FLAG_META_CONTEXT_ID) ?: hookContext.ctx?.getTargetingKey()
flagEvaluationDetails.metadata.getString(TELEMETRY_FLAG_META_FLAG_SET_ID)?.let {
attributes[TELEMETRY_FLAG_SET_ID] = it
}
flagEvaluationDetails.metadata.getString(TELEMETRY_FLAG_META_VERSION)?.let { attributes[TELEMETRY_VERSION] = it }

val variant = flagEvaluationDetails.variant
if (variant == null) {
body[TELEMETRY_BODY] = flagEvaluationDetails.value
} else {
attributes[TELEMETRY_VARIANT] = variant
}

if (flagEvaluationDetails.reason == Reason.ERROR.name) {
attributes[TELEMETRY_ERROR_CODE] = flagEvaluationDetails.errorCode ?: ErrorCode.GENERAL
flagEvaluationDetails.errorMessage?.let { attributes[TELEMETRY_ERROR_MSG] = it }
}

return EvaluationEvent(
FLAG_EVALUATION_EVENT_NAME,
attributes,
body
)
}
Loading