@@ -3,20 +3,26 @@ package otel
33import (
44 "context"
55 "fmt"
6+ "slices"
7+ "strings"
68
79 "github.com/open-feature/go-sdk/openfeature"
810 "go.opentelemetry.io/otel/attribute"
9- "go.opentelemetry.io/otel/codes"
10- semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
11+ semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
1112 "go.opentelemetry.io/otel/trace"
1213)
1314
1415// EventName is the name of the span event.
1516const EventName = "feature_flag.evaluation"
1617
18+ const (
19+ flagMetaContextIDKey string = "contextId"
20+ flagMetaFlagSetIDKey string = "flagSetId"
21+ flagMetaVersionKey string = "version"
22+ )
23+
1724// traceHook is the hook implementation for OTel traces.
1825type traceHook struct {
19- setErrorStatus bool
2026 attributeMapperCallback func (openfeature.FlagMetadata ) []attribute.KeyValue
2127
2228 openfeature.UnimplementedHook
@@ -35,48 +41,119 @@ func NewTracesHook(opts ...Options) *traceHook {
3541 return h
3642}
3743
38- // After sets the feature_flag event and associated attributes on the span stored in the context.
39- func (h * traceHook ) After (ctx context.Context , hookContext openfeature.HookContext , flagEvaluationDetails openfeature.InterfaceEvaluationDetails , hookHints openfeature.HookHints ) error {
40- attribs := []attribute.KeyValue {
44+ // Finally adds the feature_flag event and associated attributes on the span stored in the context.
45+ func (h * traceHook ) Finally (ctx context.Context , hookContext openfeature.HookContext , flagEvaluationDetails openfeature.InterfaceEvaluationDetails , hookHints openfeature.HookHints ) {
46+ attrs := eventAttributes (hookContext , flagEvaluationDetails )
47+ if h .attributeMapperCallback != nil {
48+ attrs = slices .Concat (attrs , h .attributeMapperCallback (flagEvaluationDetails .FlagMetadata ))
49+ }
50+ trace .SpanFromContext (ctx ).AddEvent (EventName , trace .WithAttributes (attrs ... ))
51+ }
52+
53+ // eventAttributes returns a slice of OpenTelemetry attributes that can be used to create an event for a feature flag evaluation.
54+ func eventAttributes (hookContext openfeature.HookContext , details openfeature.InterfaceEvaluationDetails ) []attribute.KeyValue {
55+ attrs := []attribute.KeyValue {
4156 semconv .FeatureFlagKey (hookContext .FlagKey ()),
4257 semconv .FeatureFlagProviderName (hookContext .ProviderMetadata ().Name ),
4358 }
44- if flagEvaluationDetails .Variant != "" {
45- attribs = append (attribs , semconv .FeatureFlagResultVariant (flagEvaluationDetails .Variant ))
59+
60+ switch v := details .Value .(type ) {
61+ case bool :
62+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .Bool (v ))
63+ case string :
64+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .String (v ))
65+ case int64 :
66+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .Int64 (v ))
67+ case float64 :
68+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .Float64 (v ))
69+
70+ // try to cover common types for object value supported by otel
71+ case []bool :
72+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .BoolSlice (v ))
73+ case []string :
74+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .StringSlice (v ))
75+ case []int64 :
76+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .Int64Slice (v ))
77+ case []float64 :
78+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .Float64Slice (v ))
79+ case int :
80+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .Int (v ))
81+ case []int :
82+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .IntSlice (v ))
83+ case float32 :
84+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .Float64 (float64 (v )))
85+ case []float32 :
86+ vals := make ([]float64 , len (v ))
87+ for i , val := range v {
88+ vals [i ] = float64 (val )
89+ }
90+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .Float64Slice (vals ))
91+ default :
92+ if val , ok := v .(fmt.Stringer ); ok {
93+ attrs = append (attrs , semconv .FeatureFlagResultValueKey .String (val .String ()))
94+ }
4695 }
4796
48- if h . attributeMapperCallback != nil {
49- attribs = append (attribs , h . attributeMapperCallback ( flagEvaluationDetails . FlagMetadata ) ... )
97+ if details . Variant != "" {
98+ attrs = append (attrs , semconv . FeatureFlagResultVariant ( details . Variant ) )
5099 }
51100
52- span := trace .SpanFromContext (ctx )
53- span .AddEvent (EventName , trace .WithAttributes (attribs ... ))
54- return nil
55- }
101+ switch details .Reason {
102+ case openfeature .CachedReason :
103+ attrs = append (attrs , semconv .FeatureFlagResultReasonCached )
104+ case openfeature .DefaultReason :
105+ attrs = append (attrs , semconv .FeatureFlagResultReasonDefault )
106+ case openfeature .DisabledReason :
107+ attrs = append (attrs , semconv .FeatureFlagResultReasonDisabled )
108+ case openfeature .ErrorReason :
109+ attrs = append (attrs , semconv .FeatureFlagResultReasonError )
110+ errorType := openfeature .GeneralCode
111+ if details .ErrorCode != "" {
112+ errorType = details .ErrorCode
113+ }
114+ attrs = append (attrs , semconv .ErrorTypeKey .String (
115+ strings .ToLower (string (errorType )),
116+ ))
117+
118+ if details .ErrorMessage != "" {
119+ attrs = append (attrs , semconv .ErrorMessage (details .ErrorMessage ))
120+ }
121+ case openfeature .SplitReason :
122+ attrs = append (attrs , semconv .FeatureFlagResultReasonSplit )
123+ case openfeature .StaticReason :
124+ attrs = append (attrs , semconv .FeatureFlagResultReasonStatic )
125+ case openfeature .TargetingMatchReason :
126+ attrs = append (attrs , semconv .FeatureFlagResultReasonTargetingMatch )
127+ default :
128+ attrs = append (attrs , semconv .FeatureFlagResultReasonUnknown )
129+ }
56130
57- // Error records the given error against the span and sets the span to an error status.
58- func (h * traceHook ) Error (ctx context.Context , hookContext openfeature.HookContext , err error , hookHints openfeature.HookHints ) {
59- span := trace .SpanFromContext (ctx )
131+ contextID := hookContext .EvaluationContext ().TargetingKey ()
132+ if flagMetaContextID , ok := details .FlagMetadata [flagMetaContextIDKey ].(string ); ok {
133+ contextID = flagMetaContextID
134+ }
135+ attrs = append (attrs , semconv .FeatureFlagContextID (contextID ))
60136
61- if h .setErrorStatus {
62- span .SetStatus (codes .Error ,
63- fmt .Sprintf ("error evaluating flag '%s' of type '%s'" , hookContext .FlagKey (), hookContext .FlagType ().String ()))
137+ if setID , ok := details .FlagMetadata [flagMetaFlagSetIDKey ].(string ); ok {
138+ attrs = append (attrs , semconv .FeatureFlagSetID (setID ))
64139 }
65140
66- span .RecordError (err , trace .WithAttributes (
67- semconv .FeatureFlagKey (hookContext .FlagKey ()),
68- semconv .FeatureFlagProviderName (hookContext .ProviderMetadata ().Name ),
69- ))
141+ if version , ok := details .FlagMetadata [flagMetaVersionKey ].(string ); ok {
142+ attrs = append (attrs , semconv .FeatureFlagVersion (version ))
143+ }
144+
145+ return attrs
70146}
71147
72148// Options of the hook
73149
74150type Options func (* traceHook )
75151
76152// WithErrorStatusEnabled enable setting span status to codes.Error in case of an error. Default behavior is disabled.
153+ //
154+ // Deprecated: this option has no effect. It will be removed in a future release.
77155func WithErrorStatusEnabled () Options {
78156 return func (h * traceHook ) {
79- h .setErrorStatus = true
80157 }
81158}
82159
0 commit comments