Skip to content
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
97 changes: 97 additions & 0 deletions openfeature/telemetry/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package telemetry

import (
"strings"

"github.com/open-feature/go-sdk/openfeature"
)

type EvaluationEvent struct {
Name string
Attributes map[string]any
Body map[string]any
}

const (
// The OpenTelemetry compliant event attributes for flag evaluation.
// Specification: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/

TelemetryKey string = "feature_flag.key"
TelemetryErrorCode string = "error.type"
TelemetryVariant string = "feature_flag.variant"
TelemetryContextID string = "feature_flag.context.id"
TelemetryErrorMsg string = "feature_flag.evaluation.error.message"
TelemetryReason string = "feature_flag.evaluation.reason"
TelemetryProvider string = "feature_flag.provider_name"
TelemetryFlagSetID string = "feature_flag.set.id"
TelemetryVersion string = "feature_flag.version"


// Well-known flag metadata attributes for telemetry events.
// Specification: https://openfeature.dev/specification/appendix-d#flag-metadata
TelemetryFlagMetaContextId string = "contextId"
TelemetryFlagMetaFlagSetId string = "flagSetId"
TelemetryFlagMetaVersion string = "version"

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

FlagEvaluationEventName string = "feature_flag.evaluation"
)

func CreateEvaluationEvent(hookContext openfeature.HookContext, details openfeature.InterfaceEvaluationDetails) EvaluationEvent {
attributes := map[string]any{
TelemetryKey: hookContext.FlagKey(),
TelemetryProvider: hookContext.ProviderMetadata().Name,
}

if details.EvaluationDetails.ResolutionDetail.Reason != "" {
attributes[TelemetryReason] = strings.ToLower(string(details.ResolutionDetail.Reason))
} else {
attributes[TelemetryReason] = strings.ToLower(string(openfeature.UnknownReason))
}

body := map[string]any{}

if details.Variant != "" {
attributes[TelemetryVariant] = details.EvaluationDetails.ResolutionDetail.Variant
} else {
body[TelemetryBody] = details.Value
}

contextID, exists := details.EvaluationDetails.ResolutionDetail.FlagMetadata[TelemetryFlagMetaContextId]
if !exists {
contextID = hookContext.EvaluationContext().TargetingKey()
}

attributes[TelemetryContextID] = contextID

setID, exists := details.EvaluationDetails.ResolutionDetail.FlagMetadata[TelemetryFlagMetaFlagSetId]
if exists {
attributes[TelemetryFlagSetID] = setID
}

version, exists := details.EvaluationDetails.ResolutionDetail.FlagMetadata[TelemetryFlagMetaVersion]
if exists {
attributes[TelemetryVersion] = version
}

if details.EvaluationDetails.ResolutionDetail.Reason == openfeature.ErrorReason {
if details.ResolutionDetail.ErrorCode != "" {
attributes[TelemetryErrorCode] = details.ResolutionDetail.ErrorCode
} else {
attributes[TelemetryErrorCode] = openfeature.GeneralCode
}

if details.ResolutionDetail.ErrorMessage != "" {
attributes[TelemetryErrorMsg] = details.ResolutionDetail.ErrorMessage
}
}

return EvaluationEvent{
Name: FlagEvaluationEventName,
Attributes: attributes,
Body: body,
}
}
258 changes: 258 additions & 0 deletions openfeature/telemetry/telemetry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package telemetry

import (
"strings"
"testing"

"github.com/open-feature/go-sdk/openfeature"
)

func TestCreateEvaluationEvent_1_3_1_BasicEvent(t *testing.T) {
flagKey := "test-flag"

mockProviderMetadata := openfeature.Metadata{
Name: "test-provider",
}

mockClientMetadata := openfeature.NewClientMetadata("test-client")

mockEvalCtx := openfeature.NewEvaluationContext(
"test-target-key", map[string]any{
"is": "a test",
})

mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, true, mockClientMetadata, mockProviderMetadata, mockEvalCtx)

mockDetails := openfeature.InterfaceEvaluationDetails{
Value: true,
EvaluationDetails: openfeature.EvaluationDetails{
FlagKey: flagKey,
FlagType: openfeature.Boolean,
ResolutionDetail: openfeature.ResolutionDetail{
Reason: openfeature.StaticReason,
FlagMetadata: openfeature.FlagMetadata{},
},
},
}

event := CreateEvaluationEvent(mockHookContext, mockDetails)

if event.Name != "feature_flag.evaluation" {
t.Errorf("Expected event name to be 'feature_flag.evaluation', got '%s'", event.Name)
}

if event.Attributes[TelemetryKey] != flagKey {
t.Errorf("Expected event attribute 'KEY' to be '%s', got '%s'", flagKey, event.Attributes[TelemetryKey])
}

if event.Attributes[TelemetryReason] != strings.ToLower(string(openfeature.StaticReason)) {
t.Errorf("Expected evaluation reason to be '%s', got '%s'", strings.ToLower(string(openfeature.StaticReason)), event.Attributes[TelemetryReason])
}

if event.Attributes[TelemetryProvider] != "test-provider" {
t.Errorf("Expected provider name to be 'test-provider', got '%s'", event.Attributes[TelemetryProvider])
}

if event.Body[TelemetryBody] != true {
t.Errorf("Expected event body 'VALUE' to be 'true', got '%v'", event.Body[TelemetryBody])
}
}

func TestCreateEvaluationEvent_1_4_6_WithVariant(t *testing.T) {

flagKey := "test-flag"

mockProviderMetadata := openfeature.Metadata{
Name: "test-provider",
}

mockClientMetadata := openfeature.NewClientMetadata("test-client")

mockEvalCtx := openfeature.NewEvaluationContext(
"test-target-key", map[string]any{
"is": "a test",
})

mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, true, mockClientMetadata, mockProviderMetadata, mockEvalCtx)

mockDetails := openfeature.InterfaceEvaluationDetails{
Value: true,
EvaluationDetails: openfeature.EvaluationDetails{
FlagKey: flagKey,
FlagType: openfeature.Boolean,
ResolutionDetail: openfeature.ResolutionDetail{
Variant: "true",
},
},
}

event := CreateEvaluationEvent(mockHookContext, mockDetails)

if event.Name != "feature_flag.evaluation" {
t.Errorf("Expected event name to be 'feature_flag.evaluation', got '%s'", event.Name)
}

if event.Attributes[TelemetryKey] != flagKey {
t.Errorf("Expected event attribute 'KEY' to be '%s', got '%s'", flagKey, event.Attributes[TelemetryKey])
}

if event.Attributes[TelemetryVariant] != "true" {
t.Errorf("Expected event attribute 'VARIANT' to be 'true', got '%s'", event.Attributes[TelemetryVariant])
}

}
func TestCreateEvaluationEvent_1_4_14_WithFlagMetaData(t *testing.T) {
flagKey := "test-flag"

mockProviderMetadata := openfeature.Metadata{
Name: "test-provider",
}

mockClientMetadata := openfeature.NewClientMetadata("test-client")

mockEvalCtx := openfeature.NewEvaluationContext(
"test-target-key", map[string]any{
"is": "a test",
})

mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, false, mockClientMetadata, mockProviderMetadata, mockEvalCtx)

mockDetails := openfeature.InterfaceEvaluationDetails{
Value: false,
EvaluationDetails: openfeature.EvaluationDetails{
FlagKey: flagKey,
FlagType: openfeature.Boolean,
ResolutionDetail: openfeature.ResolutionDetail{
FlagMetadata: openfeature.FlagMetadata{
TelemetryFlagMetaFlagSetId: "test-set",
TelemetryFlagMetaContextId: "metadata-context",
TelemetryFlagMetaVersion: "v1.0",
},
},
},
}

event := CreateEvaluationEvent(mockHookContext, mockDetails)

if event.Attributes[TelemetryFlagSetID] != "test-set" {
t.Errorf("Expected 'Flag SetID' in Flag Metadata name to be 'test-set', got '%s'", event.Attributes[TelemetryFlagMetaFlagSetId])
}

if event.Attributes[TelemetryContextID] != "metadata-context" {
t.Errorf("Expected 'Flag ContextID' in Flag Metadata name to be 'metadata-context', got '%s'", event.Attributes[TelemetryFlagMetaContextId])
}

if event.Attributes[TelemetryVersion] != "v1.0" {
t.Errorf("Expected 'Flag Version' in Flag Metadata name to be 'v1.0', got '%s'", event.Attributes[TelemetryFlagMetaVersion])
}
}
func TestCreateEvaluationEvent_1_4_8_WithErrors(t *testing.T) {
flagKey := "test-flag"

mockProviderMetadata := openfeature.Metadata{
Name: "test-provider",
}

mockClientMetadata := openfeature.NewClientMetadata("test-client")

mockEvalCtx := openfeature.NewEvaluationContext(
"test-target-key", map[string]any{
"is": "a test",
})

mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, false, mockClientMetadata, mockProviderMetadata, mockEvalCtx)

mockDetails := openfeature.InterfaceEvaluationDetails{
Value: false,
EvaluationDetails: openfeature.EvaluationDetails{
FlagKey: flagKey,
ResolutionDetail: openfeature.ResolutionDetail{
Reason: openfeature.ErrorReason,
ErrorCode: openfeature.FlagNotFoundCode,
ErrorMessage: "a test error",
FlagMetadata: openfeature.FlagMetadata{},
},
},
}

event := CreateEvaluationEvent(mockHookContext, mockDetails)

if event.Attributes[TelemetryErrorCode] != openfeature.FlagNotFoundCode {
t.Errorf("Expected 'ERROR_CODE' to be 'GENERAL', got '%s'", event.Attributes[TelemetryErrorCode])
}

if event.Attributes[TelemetryErrorMsg] != "a test error" {
t.Errorf("Expected 'ERROR_MESSAGE' to be 'a test error', got '%s'", event.Attributes[TelemetryErrorMsg])
}
}

func TestCreateEvaluationEvent_1_4_8_WithGeneralErrors(t *testing.T) {
flagKey := "test-flag"

mockProviderMetadata := openfeature.Metadata{
Name: "test-provider",
}

mockClientMetadata := openfeature.NewClientMetadata("test-client")

mockEvalCtx := openfeature.NewEvaluationContext(
"test-target-key", map[string]any{
"is": "a test",
})

mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, false, mockClientMetadata, mockProviderMetadata, mockEvalCtx)

mockDetails := openfeature.InterfaceEvaluationDetails{
Value: false,
EvaluationDetails: openfeature.EvaluationDetails{
FlagKey: flagKey,
ResolutionDetail: openfeature.ResolutionDetail{
Reason: openfeature.ErrorReason,
ErrorMessage: "a test error",
FlagMetadata: openfeature.FlagMetadata{},
},
},
}

event := CreateEvaluationEvent(mockHookContext, mockDetails)

if event.Attributes[TelemetryErrorCode] != openfeature.GeneralCode {
t.Errorf("Expected 'ERROR_CODE' to be 'GENERAL', got '%s'", event.Attributes[TelemetryErrorCode])
}

if event.Attributes[TelemetryErrorMsg] != "a test error" {
t.Errorf("Expected 'ERROR_MESSAGE' to be 'a test error', got '%s'", event.Attributes[TelemetryErrorMsg])
}
}
func TestCreateEvaluationEvent_1_4_7_WithUnknownReason(t *testing.T) {
flagKey := "test-flag"

mockProviderMetadata := openfeature.Metadata{
Name: "test-provider",
}

mockClientMetadata := openfeature.NewClientMetadata("test-client")

mockEvalCtx := openfeature.NewEvaluationContext(
"test-target-key", map[string]any{
"is": "a test",
})

mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, true, mockClientMetadata, mockProviderMetadata, mockEvalCtx)

mockDetails := openfeature.InterfaceEvaluationDetails{
Value: true,
EvaluationDetails: openfeature.EvaluationDetails{
FlagKey: flagKey,
ResolutionDetail: openfeature.ResolutionDetail{
FlagMetadata: openfeature.FlagMetadata{},
},
},
}

event := CreateEvaluationEvent(mockHookContext, mockDetails)

if event.Attributes[TelemetryReason] != strings.ToLower(string(openfeature.UnknownReason)) {
t.Errorf("Expected evaluation reason to be '%s', got '%s'", strings.ToLower(string(openfeature.UnknownReason)), event.Attributes[TelemetryReason])
}
}
Loading