-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add OTel event creation util func (#325)
* 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
1 parent
a55418d
commit 3c70dc2
Showing
2 changed files
with
355 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
} | ||
} |