Repository: open-feature/go-sdk (go)

Issue:
Update the OTel-Compatible Telemetry Util Function to latest spec
### Requirements

- [Match latest spec](https://github.com/open-feature/spec/pull/306)
- Update the description to include a disclaimer that this feature is part of the experimental OTel semcon.

> [!NOTE]
> This will be a breaking change, but the current implementation is still [unreleased](https://github.com/open-feature/go-sdk/pull/318).

Pull request:
feat: update telemetry package to conform to latest spec
<!-- Please use this template for your pull request. -->
<!-- Please use the sections that you need and delete other sections -->

## This PR
<!-- add the description of the PR here -->

Update telemetry package to conform to latest spec. I also refactored a bit and added doc comments.

### Related Issues
<!-- add here the GitHub issue that this PR resolves if applicable -->

Fixes #364 

### Notes
<!-- any additional notes for this PR -->

### Follow-up Tasks
<!-- anything that is related to this PR but not done here should be noted under this section -->
<!-- if there is a need for a new issue, please link it here -->

### How to test
<!-- if applicable, add testing instructions under this section -->



Patch:
diff --git a/openfeature/telemetry/telemetry.go b/openfeature/telemetry/telemetry.go
index 147eb78..14f363b 100644
--- a/openfeature/telemetry/telemetry.go
+++ b/openfeature/telemetry/telemetry.go
@@ -1,3 +1,4 @@
+// Package telemetry provides utilities for extracting data from the OpenFeature SDK for use in telemetry signals.
 package telemetry
 
 import (
@@ -6,91 +7,94 @@ import (
 	"github.com/open-feature/go-sdk/openfeature"
 )
 
+// EvaluationEvent represents an event that is emitted when a flag is evaluated.
+// It is intended to be used to record flag evaluation events as OpenTelemetry log records.
+// See the OpenFeature specification [Appendix D: Observability] and
+// the OpenTelemetry [Semantic conventions for feature flags in logs] for more information.
+//
+// [Appendix D: Observability]: https://openfeature.dev/specification/appendix-d
+// [Semantic conventions for feature flags in logs]: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
 type EvaluationEvent struct {
-	Name       string
+	// Name is the name of the event.
+	// It is always "feature_flag.evaluation".
+	Name string
+	// Attributes represents the event's attributes.
 	Attributes map[string]any
-	Body       map[string]any
 }
 
+// The OpenTelemetry compliant event attributes for flag evaluation.
 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"
+	FlagKey          string = "feature_flag.key"
+	ErrorTypeKey     string = "error.type"
+	ResultValueKey   string = "feature_flag.result.value"
+	ResultVariantKey string = "feature_flag.result.variant"
+	ErrorMessageKey  string = "error.message"
+	ContextIDKey     string = "feature_flag.context.id"
+	ProviderNameKey  string = "feature_flag.provider.name"
+	ResultReasonKey  string = "feature_flag.result.reason"
+	FlagSetIDKey     string = "feature_flag.set.id"
+	VersionKey       string = "feature_flag.version"
 )
 
+// FlagEvaluationKey is the name of the feature flag evaluation event.
+const FlagEvaluationKey string = "feature_flag.evaluation"
+
+const (
+	flagMetaContextIDKey string = "contextId"
+	flagMetaFlagSetIDKey string = "flagSetId"
+	flagMetaVersionKey   string = "version"
+)
+
+// CreateEvaluationEvent creates an [EvaluationEvent].
+// It is intended to be used in the `Finally` stage of a [openfeature.Hook].
 func CreateEvaluationEvent(hookContext openfeature.HookContext, details openfeature.InterfaceEvaluationDetails) EvaluationEvent {
 	attributes := map[string]any{
-		TelemetryKey:      hookContext.FlagKey(),
-		TelemetryProvider: hookContext.ProviderMetadata().Name,
+		FlagKey:         hookContext.FlagKey(),
+		ProviderNameKey: hookContext.ProviderMetadata().Name,
 	}
 
+	attributes[ResultReasonKey] = strings.ToLower(string(openfeature.UnknownReason))
 	if details.Reason != "" {
-		attributes[TelemetryReason] = strings.ToLower(string(details.Reason))
-	} else {
-		attributes[TelemetryReason] = strings.ToLower(string(openfeature.UnknownReason))
+		attributes[ResultReasonKey] = strings.ToLower(string(details.Reason))
 	}
 
-	body := map[string]any{}
-
 	if details.Variant != "" {
-		attributes[TelemetryVariant] = details.Variant
+		attributes[ResultVariantKey] = details.Variant
 	} else {
-		body[TelemetryBody] = details.Value
+		attributes[ResultValueKey] = details.Value
 	}
 
-	contextID, exists := details.FlagMetadata[TelemetryFlagMetaContextId]
-	if !exists {
-		contextID = hookContext.EvaluationContext().TargetingKey()
+	attributes[ContextIDKey] = hookContext.EvaluationContext().TargetingKey()
+	if contextID, ok := details.FlagMetadata[flagMetaContextIDKey]; ok {
+		attributes[ContextIDKey] = contextID
 	}
 
-	attributes[TelemetryContextID] = contextID
-
-	setID, exists := details.FlagMetadata[TelemetryFlagMetaFlagSetId]
-	if exists {
-		attributes[TelemetryFlagSetID] = setID
+	if setID, ok := details.FlagMetadata[flagMetaFlagSetIDKey]; ok {
+		attributes[FlagSetIDKey] = setID
 	}
 
-	version, exists := details.FlagMetadata[TelemetryFlagMetaVersion]
-	if exists {
-		attributes[TelemetryVersion] = version
+	if version, ok := details.FlagMetadata[flagMetaVersionKey]; ok {
+		attributes[VersionKey] = version
 	}
 
-	if details.Reason == openfeature.ErrorReason {
-		if details.ErrorCode != "" {
-			attributes[TelemetryErrorCode] = details.ErrorCode
-		} else {
-			attributes[TelemetryErrorCode] = openfeature.GeneralCode
+	if details.Reason != openfeature.ErrorReason {
+		return EvaluationEvent{
+			Name:       FlagEvaluationKey,
+			Attributes: attributes,
 		}
+	}
 
-		if details.ErrorMessage != "" {
-			attributes[TelemetryErrorMsg] = details.ErrorMessage
-		}
+	attributes[ErrorTypeKey] = strings.ToLower(string(openfeature.GeneralCode))
+	if details.ErrorCode != "" {
+		attributes[ErrorTypeKey] = strings.ToLower(string(details.ErrorCode))
+	}
+
+	if details.ErrorMessage != "" {
+		attributes[ErrorMessageKey] = details.ErrorMessage
 	}
 
 	return EvaluationEvent{
-		Name:       FlagEvaluationEventName,
+		Name:       FlagEvaluationKey,
 		Attributes: attributes,
-		Body:       body,
 	}
 }


Test patch:
diff --git a/openfeature/telemetry/telemetry_test.go b/openfeature/telemetry/telemetry_test.go
index a0c7321..0ae8bf3 100644
--- a/openfeature/telemetry/telemetry_test.go
+++ b/openfeature/telemetry/telemetry_test.go
@@ -41,25 +41,24 @@ func TestCreateEvaluationEvent_1_3_1_BasicEvent(t *testing.T) {
 		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[FlagKey] != flagKey {
+		t.Errorf("Expected event attribute 'KEY' to be '%s', got '%s'", flagKey, event.Attributes[FlagKey])
 	}
 
-	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[ResultReasonKey] != strings.ToLower(string(openfeature.StaticReason)) {
+		t.Errorf("Expected evaluation reason to be '%s', got '%s'", strings.ToLower(string(openfeature.StaticReason)), event.Attributes[ResultReasonKey])
 	}
 
-	if event.Attributes[TelemetryProvider] != "test-provider" {
-		t.Errorf("Expected provider name to be 'test-provider', got '%s'", event.Attributes[TelemetryProvider])
+	if event.Attributes[ProviderNameKey] != "test-provider" {
+		t.Errorf("Expected provider name to be 'test-provider', got '%s'", event.Attributes[ProviderNameKey])
 	}
 
-	if event.Body[TelemetryBody] != true {
-		t.Errorf("Expected event body 'VALUE' to be 'true', got '%v'", event.Body[TelemetryBody])
+	if event.Attributes[ResultValueKey] != true {
+		t.Errorf("Expected event attribute 'VALUE' to be 'true', got '%v'", event.Attributes[ResultValueKey])
 	}
 }
 
 func TestCreateEvaluationEvent_1_4_6_WithVariant(t *testing.T) {
-
 	flagKey := "test-flag"
 
 	mockProviderMetadata := openfeature.Metadata{
@@ -92,15 +91,15 @@ func TestCreateEvaluationEvent_1_4_6_WithVariant(t *testing.T) {
 		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[FlagKey] != flagKey {
+		t.Errorf("Expected event attribute 'KEY' to be '%s', got '%s'", flagKey, event.Attributes[FlagKey])
 	}
 
-	if event.Attributes[TelemetryVariant] != "true" {
-		t.Errorf("Expected event attribute 'VARIANT' to be 'true', got '%s'", event.Attributes[TelemetryVariant])
+	if event.Attributes[ResultVariantKey] != "true" {
+		t.Errorf("Expected event attribute 'VARIANT' to be 'true', got '%s'", event.Attributes[ResultVariantKey])
 	}
-
 }
+
 func TestCreateEvaluationEvent_1_4_14_WithFlagMetaData(t *testing.T) {
 	flagKey := "test-flag"
 
@@ -124,9 +123,9 @@ func TestCreateEvaluationEvent_1_4_14_WithFlagMetaData(t *testing.T) {
 			FlagType: openfeature.Boolean,
 			ResolutionDetail: openfeature.ResolutionDetail{
 				FlagMetadata: openfeature.FlagMetadata{
-					TelemetryFlagMetaFlagSetId: "test-set",
-					TelemetryFlagMetaContextId: "metadata-context",
-					TelemetryFlagMetaVersion:   "v1.0",
+					flagMetaFlagSetIDKey: "test-set",
+					flagMetaContextIDKey: "metadata-context",
+					flagMetaVersionKey:   "v1.0",
 				},
 			},
 		},
@@ -134,18 +133,19 @@ func TestCreateEvaluationEvent_1_4_14_WithFlagMetaData(t *testing.T) {
 
 	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[FlagSetIDKey] != "test-set" {
+		t.Errorf("Expected 'Flag SetID' in Flag Metadata name to be 'test-set', got '%s'", event.Attributes[flagMetaFlagSetIDKey])
 	}
 
-	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[ContextIDKey] != "metadata-context" {
+		t.Errorf("Expected 'Flag ContextID' in Flag Metadata name to be 'metadata-context', got '%s'", event.Attributes[flagMetaContextIDKey])
 	}
 
-	if event.Attributes[TelemetryVersion] != "v1.0" {
-		t.Errorf("Expected 'Flag Version' in Flag Metadata name to be 'v1.0', got '%s'", event.Attributes[TelemetryFlagMetaVersion])
+	if event.Attributes[VersionKey] != "v1.0" {
+		t.Errorf("Expected 'Flag Version' in Flag Metadata name to be 'v1.0', got '%s'", event.Attributes[flagMetaVersionKey])
 	}
 }
+
 func TestCreateEvaluationEvent_1_4_8_WithErrors(t *testing.T) {
 	flagKey := "test-flag"
 
@@ -177,12 +177,12 @@ func TestCreateEvaluationEvent_1_4_8_WithErrors(t *testing.T) {
 
 	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[ErrorTypeKey] != strings.ToLower(string(openfeature.FlagNotFoundCode)) {
+		t.Errorf("Expected 'ERROR_CODE' to be 'GENERAL', got '%s'", event.Attributes[ErrorTypeKey])
 	}
 
-	if event.Attributes[TelemetryErrorMsg] != "a test error" {
-		t.Errorf("Expected 'ERROR_MESSAGE' to be 'a test error', got '%s'", event.Attributes[TelemetryErrorMsg])
+	if event.Attributes[ErrorMessageKey] != "a test error" {
+		t.Errorf("Expected 'ERROR_MESSAGE' to be 'a test error', got '%s'", event.Attributes[ErrorMessageKey])
 	}
 }
 
@@ -216,14 +216,15 @@ func TestCreateEvaluationEvent_1_4_8_WithGeneralErrors(t *testing.T) {
 
 	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[ErrorTypeKey] != strings.ToLower(string(openfeature.GeneralCode)) {
+		t.Errorf("Expected 'ERROR_CODE' to be 'GENERAL', got '%s'", event.Attributes[ErrorTypeKey])
 	}
 
-	if event.Attributes[TelemetryErrorMsg] != "a test error" {
-		t.Errorf("Expected 'ERROR_MESSAGE' to be 'a test error', got '%s'", event.Attributes[TelemetryErrorMsg])
+	if event.Attributes[ErrorMessageKey] != "a test error" {
+		t.Errorf("Expected 'ERROR_MESSAGE' to be 'a test error', got '%s'", event.Attributes[ErrorMessageKey])
 	}
 }
+
 func TestCreateEvaluationEvent_1_4_7_WithUnknownReason(t *testing.T) {
 	flagKey := "test-flag"
 
@@ -252,7 +253,7 @@ func TestCreateEvaluationEvent_1_4_7_WithUnknownReason(t *testing.T) {
 
 	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])
+	if event.Attributes[ResultReasonKey] != strings.ToLower(string(openfeature.UnknownReason)) {
+		t.Errorf("Expected evaluation reason to be '%s', got '%s'", strings.ToLower(string(openfeature.UnknownReason)), event.Attributes[ResultReasonKey])
 	}
 }