Skip to content

feat: add OTel event creation util func #325

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 13 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from 8 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
95 changes: 95 additions & 0 deletions openfeature/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package openfeature

import (
"strings"
)

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

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 HookContext, details InterfaceEvaluationDetails) EvaluationEvent {
attributes := map[string]interface{}{
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(UnknownReason))
}

body := map[string]interface{}{}

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

contextID, exists := details.EvaluationDetails.ResolutionDetail.FlagMetadata[TelemetryFlagMetaContextId]
if exists && contextID != "" {
attributes[TelemetryFlagMetaContextId] = contextID
} else {
contextID = hookContext.evaluationContext.targetingKey

Check failure on line 65 in openfeature/telemetry.go

View workflow job for this annotation

GitHub Actions / lint

ineffectual assignment to contextID (ineffassign)
}

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

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

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

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

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

import (
"strings"
"testing"
)

func TestCreateEvaluationEvent_1_3_1_BasicEvent(t *testing.T) {
flagKey := "test-flag"
mockProviderMetadata := Metadata{
Name: "test-provider",
}
mockClientMetadata := ClientMetadata{
domain: "test-client",
}
mockHookContext := HookContext{
flagKey: flagKey,
flagType: Boolean,
defaultValue: true,
clientMetadata: mockClientMetadata,
providerMetadata: mockProviderMetadata,
}

mockDetails := InterfaceEvaluationDetails{
Value: true,
EvaluationDetails: EvaluationDetails{
FlagKey: flagKey,
FlagType: Boolean,
ResolutionDetail: ResolutionDetail{
Reason: StaticReason,
FlagMetadata: 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(StaticReason)) {
t.Errorf("Expected evaluation reason to be '%s', got '%s'", strings.ToLower(string(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 := Metadata{
Name: "test-provider",
}
mockClientMetadata := ClientMetadata{
domain: "test-client",
}
mockHookContext := HookContext{
flagKey: flagKey,
flagType: Boolean,
defaultValue: true,
clientMetadata: mockClientMetadata,
providerMetadata: mockProviderMetadata,
}

mockDetails := InterfaceEvaluationDetails{
Value: true,
EvaluationDetails: EvaluationDetails{
FlagKey: flagKey,
FlagType: Boolean,
ResolutionDetail: 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 := Metadata{
Name: "test-provider",
}
mockClientMetadata := ClientMetadata{
domain: "test-client",
}
mockHookContext := HookContext{
flagKey: flagKey,
flagType: Boolean,
defaultValue: true,
clientMetadata: mockClientMetadata,
providerMetadata: mockProviderMetadata,
}

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

event := CreateEvaluationEvent(mockHookContext, mockDetails)

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

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

if event.Attributes[TelemetryFlagMetaVersion] != "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 := Metadata{
Name: "test-provider",
}
mockClientMetadata := ClientMetadata{
domain: "test-client",
}
mockHookContext := HookContext{
flagKey: flagKey,
flagType: Boolean,
defaultValue: true,
clientMetadata: mockClientMetadata,
providerMetadata: mockProviderMetadata,
}

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

event := CreateEvaluationEvent(mockHookContext, mockDetails)

if event.Attributes[TelemetryErrorCode] != 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 := Metadata{
Name: "test-provider",
}
mockClientMetadata := ClientMetadata{
domain: "test-client",
}
mockHookContext := HookContext{
flagKey: flagKey,
flagType: Boolean,
defaultValue: true,
clientMetadata: mockClientMetadata,
providerMetadata: mockProviderMetadata,
}

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

event := CreateEvaluationEvent(mockHookContext, mockDetails)

if event.Attributes[TelemetryErrorCode] != 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 := Metadata{
Name: "test-provider",
}
mockClientMetadata := ClientMetadata{
domain: "test-client",
}
mockHookContext := HookContext{
flagKey: flagKey,
flagType: Boolean,
defaultValue: true,
clientMetadata: mockClientMetadata,
providerMetadata: mockProviderMetadata,
}

mockDetails := InterfaceEvaluationDetails{
Value: false,
EvaluationDetails: EvaluationDetails{
FlagKey: flagKey,
ResolutionDetail: ResolutionDetail{
FlagMetadata: FlagMetadata{},
},
},
}

event := CreateEvaluationEvent(mockHookContext, mockDetails)

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