Skip to content

Commit

Permalink
feat: add OTel event creation util func (#325)
Browse files Browse the repository at this point in the history
* feat:a base idea of telemetry util after looking at js version

Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com>

* style: cleaning up comments

Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com>

* feat: version and reason ser for the eval event, passing first test

Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com>

* test: writing unit tests for flag metadata and with variant

Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com>

* fix: flag metadata not being properly set causing failing variant test

Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com>

* test: writing test for unknown reason check and with errors

Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com>

* test: adding tests for General error code and test specific error code based on codecov report

Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com>

* refactor: changing variable that is never used caught by the linter to set what is being set by the if-else

Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com>

* refactor: moved telemetry to own package & updated some types for readability

Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com>

* fix: put the unused const values in the proper attributes

Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com>

---------

Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
  • Loading branch information
3 people authored Feb 27, 2025
1 parent a55418d commit 3c70dc2
Show file tree
Hide file tree
Showing 2 changed files with 355 additions and 0 deletions.
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])
}
}

0 comments on commit 3c70dc2

Please sign in to comment.