Skip to content

Commit 3c70dc2

Browse files
bbland1beeme1mrtoddbaert
authored
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>
1 parent a55418d commit 3c70dc2

File tree

2 files changed

+355
-0
lines changed

2 files changed

+355
-0
lines changed

openfeature/telemetry/telemetry.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package telemetry
2+
3+
import (
4+
"strings"
5+
6+
"github.com/open-feature/go-sdk/openfeature"
7+
)
8+
9+
type EvaluationEvent struct {
10+
Name string
11+
Attributes map[string]any
12+
Body map[string]any
13+
}
14+
15+
const (
16+
// The OpenTelemetry compliant event attributes for flag evaluation.
17+
// Specification: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
18+
19+
TelemetryKey string = "feature_flag.key"
20+
TelemetryErrorCode string = "error.type"
21+
TelemetryVariant string = "feature_flag.variant"
22+
TelemetryContextID string = "feature_flag.context.id"
23+
TelemetryErrorMsg string = "feature_flag.evaluation.error.message"
24+
TelemetryReason string = "feature_flag.evaluation.reason"
25+
TelemetryProvider string = "feature_flag.provider_name"
26+
TelemetryFlagSetID string = "feature_flag.set.id"
27+
TelemetryVersion string = "feature_flag.version"
28+
29+
30+
// Well-known flag metadata attributes for telemetry events.
31+
// Specification: https://openfeature.dev/specification/appendix-d#flag-metadata
32+
TelemetryFlagMetaContextId string = "contextId"
33+
TelemetryFlagMetaFlagSetId string = "flagSetId"
34+
TelemetryFlagMetaVersion string = "version"
35+
36+
// OpenTelemetry event body.
37+
// Specification: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
38+
TelemetryBody string = "value"
39+
40+
FlagEvaluationEventName string = "feature_flag.evaluation"
41+
)
42+
43+
func CreateEvaluationEvent(hookContext openfeature.HookContext, details openfeature.InterfaceEvaluationDetails) EvaluationEvent {
44+
attributes := map[string]any{
45+
TelemetryKey: hookContext.FlagKey(),
46+
TelemetryProvider: hookContext.ProviderMetadata().Name,
47+
}
48+
49+
if details.EvaluationDetails.ResolutionDetail.Reason != "" {
50+
attributes[TelemetryReason] = strings.ToLower(string(details.ResolutionDetail.Reason))
51+
} else {
52+
attributes[TelemetryReason] = strings.ToLower(string(openfeature.UnknownReason))
53+
}
54+
55+
body := map[string]any{}
56+
57+
if details.Variant != "" {
58+
attributes[TelemetryVariant] = details.EvaluationDetails.ResolutionDetail.Variant
59+
} else {
60+
body[TelemetryBody] = details.Value
61+
}
62+
63+
contextID, exists := details.EvaluationDetails.ResolutionDetail.FlagMetadata[TelemetryFlagMetaContextId]
64+
if !exists {
65+
contextID = hookContext.EvaluationContext().TargetingKey()
66+
}
67+
68+
attributes[TelemetryContextID] = contextID
69+
70+
setID, exists := details.EvaluationDetails.ResolutionDetail.FlagMetadata[TelemetryFlagMetaFlagSetId]
71+
if exists {
72+
attributes[TelemetryFlagSetID] = setID
73+
}
74+
75+
version, exists := details.EvaluationDetails.ResolutionDetail.FlagMetadata[TelemetryFlagMetaVersion]
76+
if exists {
77+
attributes[TelemetryVersion] = version
78+
}
79+
80+
if details.EvaluationDetails.ResolutionDetail.Reason == openfeature.ErrorReason {
81+
if details.ResolutionDetail.ErrorCode != "" {
82+
attributes[TelemetryErrorCode] = details.ResolutionDetail.ErrorCode
83+
} else {
84+
attributes[TelemetryErrorCode] = openfeature.GeneralCode
85+
}
86+
87+
if details.ResolutionDetail.ErrorMessage != "" {
88+
attributes[TelemetryErrorMsg] = details.ResolutionDetail.ErrorMessage
89+
}
90+
}
91+
92+
return EvaluationEvent{
93+
Name: FlagEvaluationEventName,
94+
Attributes: attributes,
95+
Body: body,
96+
}
97+
}
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
package telemetry
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/open-feature/go-sdk/openfeature"
8+
)
9+
10+
func TestCreateEvaluationEvent_1_3_1_BasicEvent(t *testing.T) {
11+
flagKey := "test-flag"
12+
13+
mockProviderMetadata := openfeature.Metadata{
14+
Name: "test-provider",
15+
}
16+
17+
mockClientMetadata := openfeature.NewClientMetadata("test-client")
18+
19+
mockEvalCtx := openfeature.NewEvaluationContext(
20+
"test-target-key", map[string]any{
21+
"is": "a test",
22+
})
23+
24+
mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, true, mockClientMetadata, mockProviderMetadata, mockEvalCtx)
25+
26+
mockDetails := openfeature.InterfaceEvaluationDetails{
27+
Value: true,
28+
EvaluationDetails: openfeature.EvaluationDetails{
29+
FlagKey: flagKey,
30+
FlagType: openfeature.Boolean,
31+
ResolutionDetail: openfeature.ResolutionDetail{
32+
Reason: openfeature.StaticReason,
33+
FlagMetadata: openfeature.FlagMetadata{},
34+
},
35+
},
36+
}
37+
38+
event := CreateEvaluationEvent(mockHookContext, mockDetails)
39+
40+
if event.Name != "feature_flag.evaluation" {
41+
t.Errorf("Expected event name to be 'feature_flag.evaluation', got '%s'", event.Name)
42+
}
43+
44+
if event.Attributes[TelemetryKey] != flagKey {
45+
t.Errorf("Expected event attribute 'KEY' to be '%s', got '%s'", flagKey, event.Attributes[TelemetryKey])
46+
}
47+
48+
if event.Attributes[TelemetryReason] != strings.ToLower(string(openfeature.StaticReason)) {
49+
t.Errorf("Expected evaluation reason to be '%s', got '%s'", strings.ToLower(string(openfeature.StaticReason)), event.Attributes[TelemetryReason])
50+
}
51+
52+
if event.Attributes[TelemetryProvider] != "test-provider" {
53+
t.Errorf("Expected provider name to be 'test-provider', got '%s'", event.Attributes[TelemetryProvider])
54+
}
55+
56+
if event.Body[TelemetryBody] != true {
57+
t.Errorf("Expected event body 'VALUE' to be 'true', got '%v'", event.Body[TelemetryBody])
58+
}
59+
}
60+
61+
func TestCreateEvaluationEvent_1_4_6_WithVariant(t *testing.T) {
62+
63+
flagKey := "test-flag"
64+
65+
mockProviderMetadata := openfeature.Metadata{
66+
Name: "test-provider",
67+
}
68+
69+
mockClientMetadata := openfeature.NewClientMetadata("test-client")
70+
71+
mockEvalCtx := openfeature.NewEvaluationContext(
72+
"test-target-key", map[string]any{
73+
"is": "a test",
74+
})
75+
76+
mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, true, mockClientMetadata, mockProviderMetadata, mockEvalCtx)
77+
78+
mockDetails := openfeature.InterfaceEvaluationDetails{
79+
Value: true,
80+
EvaluationDetails: openfeature.EvaluationDetails{
81+
FlagKey: flagKey,
82+
FlagType: openfeature.Boolean,
83+
ResolutionDetail: openfeature.ResolutionDetail{
84+
Variant: "true",
85+
},
86+
},
87+
}
88+
89+
event := CreateEvaluationEvent(mockHookContext, mockDetails)
90+
91+
if event.Name != "feature_flag.evaluation" {
92+
t.Errorf("Expected event name to be 'feature_flag.evaluation', got '%s'", event.Name)
93+
}
94+
95+
if event.Attributes[TelemetryKey] != flagKey {
96+
t.Errorf("Expected event attribute 'KEY' to be '%s', got '%s'", flagKey, event.Attributes[TelemetryKey])
97+
}
98+
99+
if event.Attributes[TelemetryVariant] != "true" {
100+
t.Errorf("Expected event attribute 'VARIANT' to be 'true', got '%s'", event.Attributes[TelemetryVariant])
101+
}
102+
103+
}
104+
func TestCreateEvaluationEvent_1_4_14_WithFlagMetaData(t *testing.T) {
105+
flagKey := "test-flag"
106+
107+
mockProviderMetadata := openfeature.Metadata{
108+
Name: "test-provider",
109+
}
110+
111+
mockClientMetadata := openfeature.NewClientMetadata("test-client")
112+
113+
mockEvalCtx := openfeature.NewEvaluationContext(
114+
"test-target-key", map[string]any{
115+
"is": "a test",
116+
})
117+
118+
mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, false, mockClientMetadata, mockProviderMetadata, mockEvalCtx)
119+
120+
mockDetails := openfeature.InterfaceEvaluationDetails{
121+
Value: false,
122+
EvaluationDetails: openfeature.EvaluationDetails{
123+
FlagKey: flagKey,
124+
FlagType: openfeature.Boolean,
125+
ResolutionDetail: openfeature.ResolutionDetail{
126+
FlagMetadata: openfeature.FlagMetadata{
127+
TelemetryFlagMetaFlagSetId: "test-set",
128+
TelemetryFlagMetaContextId: "metadata-context",
129+
TelemetryFlagMetaVersion: "v1.0",
130+
},
131+
},
132+
},
133+
}
134+
135+
event := CreateEvaluationEvent(mockHookContext, mockDetails)
136+
137+
if event.Attributes[TelemetryFlagSetID] != "test-set" {
138+
t.Errorf("Expected 'Flag SetID' in Flag Metadata name to be 'test-set', got '%s'", event.Attributes[TelemetryFlagMetaFlagSetId])
139+
}
140+
141+
if event.Attributes[TelemetryContextID] != "metadata-context" {
142+
t.Errorf("Expected 'Flag ContextID' in Flag Metadata name to be 'metadata-context', got '%s'", event.Attributes[TelemetryFlagMetaContextId])
143+
}
144+
145+
if event.Attributes[TelemetryVersion] != "v1.0" {
146+
t.Errorf("Expected 'Flag Version' in Flag Metadata name to be 'v1.0', got '%s'", event.Attributes[TelemetryFlagMetaVersion])
147+
}
148+
}
149+
func TestCreateEvaluationEvent_1_4_8_WithErrors(t *testing.T) {
150+
flagKey := "test-flag"
151+
152+
mockProviderMetadata := openfeature.Metadata{
153+
Name: "test-provider",
154+
}
155+
156+
mockClientMetadata := openfeature.NewClientMetadata("test-client")
157+
158+
mockEvalCtx := openfeature.NewEvaluationContext(
159+
"test-target-key", map[string]any{
160+
"is": "a test",
161+
})
162+
163+
mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, false, mockClientMetadata, mockProviderMetadata, mockEvalCtx)
164+
165+
mockDetails := openfeature.InterfaceEvaluationDetails{
166+
Value: false,
167+
EvaluationDetails: openfeature.EvaluationDetails{
168+
FlagKey: flagKey,
169+
ResolutionDetail: openfeature.ResolutionDetail{
170+
Reason: openfeature.ErrorReason,
171+
ErrorCode: openfeature.FlagNotFoundCode,
172+
ErrorMessage: "a test error",
173+
FlagMetadata: openfeature.FlagMetadata{},
174+
},
175+
},
176+
}
177+
178+
event := CreateEvaluationEvent(mockHookContext, mockDetails)
179+
180+
if event.Attributes[TelemetryErrorCode] != openfeature.FlagNotFoundCode {
181+
t.Errorf("Expected 'ERROR_CODE' to be 'GENERAL', got '%s'", event.Attributes[TelemetryErrorCode])
182+
}
183+
184+
if event.Attributes[TelemetryErrorMsg] != "a test error" {
185+
t.Errorf("Expected 'ERROR_MESSAGE' to be 'a test error', got '%s'", event.Attributes[TelemetryErrorMsg])
186+
}
187+
}
188+
189+
func TestCreateEvaluationEvent_1_4_8_WithGeneralErrors(t *testing.T) {
190+
flagKey := "test-flag"
191+
192+
mockProviderMetadata := openfeature.Metadata{
193+
Name: "test-provider",
194+
}
195+
196+
mockClientMetadata := openfeature.NewClientMetadata("test-client")
197+
198+
mockEvalCtx := openfeature.NewEvaluationContext(
199+
"test-target-key", map[string]any{
200+
"is": "a test",
201+
})
202+
203+
mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, false, mockClientMetadata, mockProviderMetadata, mockEvalCtx)
204+
205+
mockDetails := openfeature.InterfaceEvaluationDetails{
206+
Value: false,
207+
EvaluationDetails: openfeature.EvaluationDetails{
208+
FlagKey: flagKey,
209+
ResolutionDetail: openfeature.ResolutionDetail{
210+
Reason: openfeature.ErrorReason,
211+
ErrorMessage: "a test error",
212+
FlagMetadata: openfeature.FlagMetadata{},
213+
},
214+
},
215+
}
216+
217+
event := CreateEvaluationEvent(mockHookContext, mockDetails)
218+
219+
if event.Attributes[TelemetryErrorCode] != openfeature.GeneralCode {
220+
t.Errorf("Expected 'ERROR_CODE' to be 'GENERAL', got '%s'", event.Attributes[TelemetryErrorCode])
221+
}
222+
223+
if event.Attributes[TelemetryErrorMsg] != "a test error" {
224+
t.Errorf("Expected 'ERROR_MESSAGE' to be 'a test error', got '%s'", event.Attributes[TelemetryErrorMsg])
225+
}
226+
}
227+
func TestCreateEvaluationEvent_1_4_7_WithUnknownReason(t *testing.T) {
228+
flagKey := "test-flag"
229+
230+
mockProviderMetadata := openfeature.Metadata{
231+
Name: "test-provider",
232+
}
233+
234+
mockClientMetadata := openfeature.NewClientMetadata("test-client")
235+
236+
mockEvalCtx := openfeature.NewEvaluationContext(
237+
"test-target-key", map[string]any{
238+
"is": "a test",
239+
})
240+
241+
mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, true, mockClientMetadata, mockProviderMetadata, mockEvalCtx)
242+
243+
mockDetails := openfeature.InterfaceEvaluationDetails{
244+
Value: true,
245+
EvaluationDetails: openfeature.EvaluationDetails{
246+
FlagKey: flagKey,
247+
ResolutionDetail: openfeature.ResolutionDetail{
248+
FlagMetadata: openfeature.FlagMetadata{},
249+
},
250+
},
251+
}
252+
253+
event := CreateEvaluationEvent(mockHookContext, mockDetails)
254+
255+
if event.Attributes[TelemetryReason] != strings.ToLower(string(openfeature.UnknownReason)) {
256+
t.Errorf("Expected evaluation reason to be '%s', got '%s'", strings.ToLower(string(openfeature.UnknownReason)), event.Attributes[TelemetryReason])
257+
}
258+
}

0 commit comments

Comments
 (0)