Skip to content

Commit 081a04e

Browse files
committed
feat!: telemetry utils (#238)
Signed-off-by: Marcin Stepien <marcin.stepien@fluxon.com>
1 parent 2f1b956 commit 081a04e

9 files changed

Lines changed: 464 additions & 598 deletions

File tree

kotlin-sdk/api/android/kotlin-sdk.api

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,6 @@ public abstract interface class dev/openfeature/kotlin/sdk/EvaluationContext : d
3333
public abstract fun withTargetingKey (Ljava/lang/String;)Ldev/openfeature/kotlin/sdk/EvaluationContext;
3434
}
3535

36-
public final class dev/openfeature/kotlin/sdk/EvaluationEvent {
37-
public fun <init> (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V
38-
public final fun component1 ()Ljava/lang/String;
39-
public final fun component2 ()Ljava/util/Map;
40-
public final fun component3 ()Ljava/util/Map;
41-
public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)Ldev/openfeature/kotlin/sdk/EvaluationEvent;
42-
public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/EvaluationEvent;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/EvaluationEvent;
43-
public fun equals (Ljava/lang/Object;)Z
44-
public final fun getAttributes ()Ljava/util/Map;
45-
public final fun getBody ()Ljava/util/Map;
46-
public final fun getName ()Ljava/lang/String;
47-
public fun hashCode ()I
48-
public fun toString ()Ljava/lang/String;
49-
}
50-
5136
public final class dev/openfeature/kotlin/sdk/EvaluationMetadata {
5237
public static final field Companion Ldev/openfeature/kotlin/sdk/EvaluationMetadata$Companion;
5338
public fun equals (Ljava/lang/Object;)Z
@@ -411,24 +396,6 @@ public abstract interface class dev/openfeature/kotlin/sdk/Structure {
411396
public abstract fun keySet ()Ljava/util/Set;
412397
}
413398

414-
public final class dev/openfeature/kotlin/sdk/TelemetryKt {
415-
public static final field FLAG_EVALUATION_EVENT_NAME Ljava/lang/String;
416-
public static final field TELEMETRY_BODY Ljava/lang/String;
417-
public static final field TELEMETRY_CONTEXT_ID Ljava/lang/String;
418-
public static final field TELEMETRY_ERROR_CODE Ljava/lang/String;
419-
public static final field TELEMETRY_ERROR_MSG Ljava/lang/String;
420-
public static final field TELEMETRY_FLAG_META_CONTEXT_ID Ljava/lang/String;
421-
public static final field TELEMETRY_FLAG_META_FLAG_SET_ID Ljava/lang/String;
422-
public static final field TELEMETRY_FLAG_META_VERSION Ljava/lang/String;
423-
public static final field TELEMETRY_FLAG_SET_ID Ljava/lang/String;
424-
public static final field TELEMETRY_KEY Ljava/lang/String;
425-
public static final field TELEMETRY_PROVIDER Ljava/lang/String;
426-
public static final field TELEMETRY_REASON Ljava/lang/String;
427-
public static final field TELEMETRY_VARIANT Ljava/lang/String;
428-
public static final field TELEMETRY_VERSION Ljava/lang/String;
429-
public static final fun createEvaluationEvent (Ldev/openfeature/kotlin/sdk/HookContext;Ldev/openfeature/kotlin/sdk/FlagEvaluationDetails;)Ldev/openfeature/kotlin/sdk/EvaluationEvent;
430-
}
431-
432399
public abstract interface class dev/openfeature/kotlin/sdk/Tracking {
433400
public abstract fun track (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/TrackingEventDetails;)V
434401
public static synthetic fun track$default (Ldev/openfeature/kotlin/sdk/Tracking;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/TrackingEventDetails;ILjava/lang/Object;)V
@@ -915,3 +882,38 @@ public abstract interface class dev/openfeature/kotlin/sdk/multiprovider/MultiPr
915882
public abstract fun evaluate (Ljava/util/List;Ljava/lang/String;Ljava/lang/Object;Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/jvm/functions/Function4;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
916883
}
917884

885+
public final class dev/openfeature/kotlin/sdk/telemetry/EvaluationEvent {
886+
public fun <init> (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V
887+
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
888+
public final fun component1 ()Ljava/lang/String;
889+
public final fun component2 ()Ljava/util/Map;
890+
public final fun component3 ()Ljava/util/Map;
891+
public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)Ldev/openfeature/kotlin/sdk/telemetry/EvaluationEvent;
892+
public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/telemetry/EvaluationEvent;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/telemetry/EvaluationEvent;
893+
public fun equals (Ljava/lang/Object;)Z
894+
public final fun getAttributes ()Ljava/util/Map;
895+
public final fun getBody ()Ljava/util/Map;
896+
public final fun getName ()Ljava/lang/String;
897+
public fun hashCode ()I
898+
public fun toString ()Ljava/lang/String;
899+
}
900+
901+
public final class dev/openfeature/kotlin/sdk/telemetry/Telemetry {
902+
public static final field FLAG_EVALUATION_EVENT_NAME Ljava/lang/String;
903+
public static final field INSTANCE Ldev/openfeature/kotlin/sdk/telemetry/Telemetry;
904+
public static final field TELEMETRY_CONTEXT_ID Ljava/lang/String;
905+
public static final field TELEMETRY_ERROR_CODE Ljava/lang/String;
906+
public static final field TELEMETRY_ERROR_MSG Ljava/lang/String;
907+
public static final field TELEMETRY_FLAG_META_CONTEXT_ID Ljava/lang/String;
908+
public static final field TELEMETRY_FLAG_META_FLAG_SET_ID Ljava/lang/String;
909+
public static final field TELEMETRY_FLAG_META_VERSION Ljava/lang/String;
910+
public static final field TELEMETRY_FLAG_SET_ID Ljava/lang/String;
911+
public static final field TELEMETRY_KEY Ljava/lang/String;
912+
public static final field TELEMETRY_PROVIDER Ljava/lang/String;
913+
public static final field TELEMETRY_REASON Ljava/lang/String;
914+
public static final field TELEMETRY_VALUE Ljava/lang/String;
915+
public static final field TELEMETRY_VARIANT Ljava/lang/String;
916+
public static final field TELEMETRY_VERSION Ljava/lang/String;
917+
public final fun createEvaluationEvent (Ldev/openfeature/kotlin/sdk/HookContext;Ldev/openfeature/kotlin/sdk/FlagEvaluationDetails;)Ldev/openfeature/kotlin/sdk/telemetry/EvaluationEvent;
918+
}
919+

kotlin-sdk/api/jvm/kotlin-sdk.api

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,6 @@ public abstract interface class dev/openfeature/kotlin/sdk/EvaluationContext : d
3333
public abstract fun withTargetingKey (Ljava/lang/String;)Ldev/openfeature/kotlin/sdk/EvaluationContext;
3434
}
3535

36-
public final class dev/openfeature/kotlin/sdk/EvaluationEvent {
37-
public fun <init> (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V
38-
public final fun component1 ()Ljava/lang/String;
39-
public final fun component2 ()Ljava/util/Map;
40-
public final fun component3 ()Ljava/util/Map;
41-
public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)Ldev/openfeature/kotlin/sdk/EvaluationEvent;
42-
public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/EvaluationEvent;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/EvaluationEvent;
43-
public fun equals (Ljava/lang/Object;)Z
44-
public final fun getAttributes ()Ljava/util/Map;
45-
public final fun getBody ()Ljava/util/Map;
46-
public final fun getName ()Ljava/lang/String;
47-
public fun hashCode ()I
48-
public fun toString ()Ljava/lang/String;
49-
}
50-
5136
public final class dev/openfeature/kotlin/sdk/EvaluationMetadata {
5237
public static final field Companion Ldev/openfeature/kotlin/sdk/EvaluationMetadata$Companion;
5338
public fun equals (Ljava/lang/Object;)Z
@@ -411,24 +396,6 @@ public abstract interface class dev/openfeature/kotlin/sdk/Structure {
411396
public abstract fun keySet ()Ljava/util/Set;
412397
}
413398

414-
public final class dev/openfeature/kotlin/sdk/TelemetryKt {
415-
public static final field FLAG_EVALUATION_EVENT_NAME Ljava/lang/String;
416-
public static final field TELEMETRY_BODY Ljava/lang/String;
417-
public static final field TELEMETRY_CONTEXT_ID Ljava/lang/String;
418-
public static final field TELEMETRY_ERROR_CODE Ljava/lang/String;
419-
public static final field TELEMETRY_ERROR_MSG Ljava/lang/String;
420-
public static final field TELEMETRY_FLAG_META_CONTEXT_ID Ljava/lang/String;
421-
public static final field TELEMETRY_FLAG_META_FLAG_SET_ID Ljava/lang/String;
422-
public static final field TELEMETRY_FLAG_META_VERSION Ljava/lang/String;
423-
public static final field TELEMETRY_FLAG_SET_ID Ljava/lang/String;
424-
public static final field TELEMETRY_KEY Ljava/lang/String;
425-
public static final field TELEMETRY_PROVIDER Ljava/lang/String;
426-
public static final field TELEMETRY_REASON Ljava/lang/String;
427-
public static final field TELEMETRY_VARIANT Ljava/lang/String;
428-
public static final field TELEMETRY_VERSION Ljava/lang/String;
429-
public static final fun createEvaluationEvent (Ldev/openfeature/kotlin/sdk/HookContext;Ldev/openfeature/kotlin/sdk/FlagEvaluationDetails;)Ldev/openfeature/kotlin/sdk/EvaluationEvent;
430-
}
431-
432399
public abstract interface class dev/openfeature/kotlin/sdk/Tracking {
433400
public abstract fun track (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/TrackingEventDetails;)V
434401
public static synthetic fun track$default (Ldev/openfeature/kotlin/sdk/Tracking;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/TrackingEventDetails;ILjava/lang/Object;)V
@@ -915,3 +882,38 @@ public abstract interface class dev/openfeature/kotlin/sdk/multiprovider/MultiPr
915882
public abstract fun evaluate (Ljava/util/List;Ljava/lang/String;Ljava/lang/Object;Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/jvm/functions/Function4;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
916883
}
917884

885+
public final class dev/openfeature/kotlin/sdk/telemetry/EvaluationEvent {
886+
public fun <init> (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V
887+
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
888+
public final fun component1 ()Ljava/lang/String;
889+
public final fun component2 ()Ljava/util/Map;
890+
public final fun component3 ()Ljava/util/Map;
891+
public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)Ldev/openfeature/kotlin/sdk/telemetry/EvaluationEvent;
892+
public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/telemetry/EvaluationEvent;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/telemetry/EvaluationEvent;
893+
public fun equals (Ljava/lang/Object;)Z
894+
public final fun getAttributes ()Ljava/util/Map;
895+
public final fun getBody ()Ljava/util/Map;
896+
public final fun getName ()Ljava/lang/String;
897+
public fun hashCode ()I
898+
public fun toString ()Ljava/lang/String;
899+
}
900+
901+
public final class dev/openfeature/kotlin/sdk/telemetry/Telemetry {
902+
public static final field FLAG_EVALUATION_EVENT_NAME Ljava/lang/String;
903+
public static final field INSTANCE Ldev/openfeature/kotlin/sdk/telemetry/Telemetry;
904+
public static final field TELEMETRY_CONTEXT_ID Ljava/lang/String;
905+
public static final field TELEMETRY_ERROR_CODE Ljava/lang/String;
906+
public static final field TELEMETRY_ERROR_MSG Ljava/lang/String;
907+
public static final field TELEMETRY_FLAG_META_CONTEXT_ID Ljava/lang/String;
908+
public static final field TELEMETRY_FLAG_META_FLAG_SET_ID Ljava/lang/String;
909+
public static final field TELEMETRY_FLAG_META_VERSION Ljava/lang/String;
910+
public static final field TELEMETRY_FLAG_SET_ID Ljava/lang/String;
911+
public static final field TELEMETRY_KEY Ljava/lang/String;
912+
public static final field TELEMETRY_PROVIDER Ljava/lang/String;
913+
public static final field TELEMETRY_REASON Ljava/lang/String;
914+
public static final field TELEMETRY_VALUE Ljava/lang/String;
915+
public static final field TELEMETRY_VARIANT Ljava/lang/String;
916+
public static final field TELEMETRY_VERSION Ljava/lang/String;
917+
public final fun createEvaluationEvent (Ldev/openfeature/kotlin/sdk/HookContext;Ldev/openfeature/kotlin/sdk/FlagEvaluationDetails;)Ldev/openfeature/kotlin/sdk/telemetry/EvaluationEvent;
918+
}
919+

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/EvaluationEvent.kt

Lines changed: 0 additions & 27 deletions
This file was deleted.

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/Telemetry.kt

Lines changed: 0 additions & 64 deletions
This file was deleted.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package dev.openfeature.kotlin.sdk.telemetry
2+
3+
/**
4+
* Represents an evaluation event containing standard OTel flag mapping attributes.
5+
*/
6+
data class EvaluationEvent(
7+
val name: String,
8+
val attributes: Map<String, Any?>,
9+
val body: Map<String, Any?> = emptyMap()
10+
)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package dev.openfeature.kotlin.sdk.telemetry
2+
3+
import dev.openfeature.kotlin.sdk.FlagEvaluationDetails
4+
import dev.openfeature.kotlin.sdk.HookContext
5+
import dev.openfeature.kotlin.sdk.Reason
6+
import dev.openfeature.kotlin.sdk.Value
7+
import dev.openfeature.kotlin.sdk.exceptions.ErrorCode
8+
9+
/**
10+
* The Telemetry object provides constants and utilities for creating OpenTelemetry compliant
11+
* evaluation events in alignment with OpenFeature Appendix D semantics.
12+
*/
13+
object Telemetry {
14+
15+
// OTEL Semantic Convention keys for feature flag evaluation records
16+
const val TELEMETRY_KEY = "feature_flag.key"
17+
const val TELEMETRY_PROVIDER = "feature_flag.provider.name"
18+
const val TELEMETRY_REASON = "feature_flag.result.reason"
19+
const val TELEMETRY_VARIANT = "feature_flag.result.variant"
20+
const val TELEMETRY_VALUE = "feature_flag.result.value"
21+
const val TELEMETRY_CONTEXT_ID = "feature_flag.context.id"
22+
const val TELEMETRY_FLAG_SET_ID = "feature_flag.set.id"
23+
const val TELEMETRY_VERSION = "feature_flag.version"
24+
const val TELEMETRY_ERROR_CODE = "error.type"
25+
const val TELEMETRY_ERROR_MSG = "feature_flag.evaluation.error.message"
26+
27+
// OpenFeature internal metadata keys matching Spec standard mapping boundaries
28+
const val TELEMETRY_FLAG_META_CONTEXT_ID = "contextId"
29+
const val TELEMETRY_FLAG_META_FLAG_SET_ID = "flagSetId"
30+
const val TELEMETRY_FLAG_META_VERSION = "version"
31+
32+
const val FLAG_EVALUATION_EVENT_NAME = "feature_flag.evaluation"
33+
34+
/**
35+
* Creates an OpenTelemetry compliant EvaluationEvent out of standard Evaluation details.
36+
*/
37+
fun <T> createEvaluationEvent(
38+
hookContext: HookContext<T>,
39+
evaluationDetails: FlagEvaluationDetails<T>
40+
): EvaluationEvent {
41+
val attributes = mutableMapOf<String, Any?>()
42+
val body = mutableMapOf<String, Any?>()
43+
44+
// Required telemetry attributes
45+
attributes[TELEMETRY_KEY] = hookContext.flagKey
46+
attributes[TELEMETRY_PROVIDER] = hookContext.providerMetadata.name
47+
48+
// Reason (Conditionally Required / Recommended)
49+
attributes[TELEMETRY_REASON] = evaluationDetails.reason?.lowercase() ?: Reason.UNKNOWN.name.lowercase()
50+
51+
// Variant
52+
val variant = evaluationDetails.variant
53+
if (variant != null) {
54+
attributes[TELEMETRY_VARIANT] = variant
55+
}
56+
57+
// Value (Recommended even when Variant is present)
58+
attributes[TELEMETRY_VALUE] = unwrapValue(evaluationDetails.value)
59+
60+
// Context ID
61+
val contextId = evaluationDetails.metadata.getString(TELEMETRY_FLAG_META_CONTEXT_ID)
62+
?: hookContext.ctx?.getTargetingKey()
63+
if (!contextId.isNullOrEmpty()) {
64+
attributes[TELEMETRY_CONTEXT_ID] = contextId
65+
}
66+
67+
// Flag Set ID
68+
val setId = evaluationDetails.metadata.getString(TELEMETRY_FLAG_META_FLAG_SET_ID)
69+
if (setId != null) {
70+
attributes[TELEMETRY_FLAG_SET_ID] = setId
71+
}
72+
73+
// Version
74+
val version = evaluationDetails.metadata.getString(TELEMETRY_FLAG_META_VERSION)
75+
if (version != null) {
76+
attributes[TELEMETRY_VERSION] = version
77+
}
78+
79+
// Error State mapping
80+
if (evaluationDetails.reason.equals(Reason.ERROR.name, ignoreCase = true)) {
81+
attributes[TELEMETRY_ERROR_CODE] = evaluationDetails.errorCode?.name?.lowercase()
82+
?: ErrorCode.GENERAL.name.lowercase()
83+
val errorMessage = evaluationDetails.errorMessage
84+
if (errorMessage != null) {
85+
attributes[TELEMETRY_ERROR_MSG] = errorMessage
86+
}
87+
}
88+
89+
return EvaluationEvent(FLAG_EVALUATION_EVENT_NAME, attributes, body)
90+
}
91+
92+
/**
93+
* Recursively unwraps structurally-typed OpenFeature [Value] allocations organically down
94+
* strictly representing their primitive equivalents mapping gracefully for OTEL.
95+
*/
96+
@OptIn(kotlin.time.ExperimentalTime::class)
97+
private fun unwrapValue(value: Any?): Any? {
98+
return when (value) {
99+
is Value.Null -> null
100+
is Value.String -> value.string
101+
is Value.Boolean -> value.boolean
102+
is Value.Integer -> value.integer
103+
is Value.Double -> value.double
104+
is Value.Instant -> value.instant.toString() // ISO 8601 string
105+
is Value.List -> value.list.map { unwrapValue(it) }
106+
is Value.Structure -> value.structure.mapValues { (_, v) -> unwrapValue(v) }
107+
else -> value // Already a generic type primitive like standard Boolean/String evaluations
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)