Skip to content

Commit dbd6cb5

Browse files
erkasahidvelji
andauthored
refactor(hooks): align otel traces hook with feature flag spec (#796)
Signed-off-by: Roman Dmytrenko <[email protected]> Co-authored-by: Sahid Velji <[email protected]>
1 parent 0dc6b5d commit dbd6cb5

File tree

4 files changed

+478
-264
lines changed

4 files changed

+478
-264
lines changed

hooks/open-telemetry/README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,10 @@ span.End()
103103

104104
### Options
105105

106-
#### WithErrorStatusEnabled
107-
108-
Enable setting span status to `Error` in case of an error. Default behavior is disabled, span status is unset for errors.
109-
110106
#### WithTracesAttributeSetter
111107

112108
This constructor options allows to provide a custom callback to extract dimensions from `FlagMetadata`.
113-
These attributes are added at the `After` stage of the hook.
109+
These attributes are added at the `Finally` stage of the hook.
114110

115111
```go
116112

hooks/open-telemetry/go.sum

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,49 +9,31 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
99
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
1010
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
1111
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
12-
github.com/open-feature/go-sdk v1.15.1 h1:TC3FtHtOKlGlIbSf3SEpxXVhgTd/bCbuc39XHIyltkw=
13-
github.com/open-feature/go-sdk v1.15.1/go.mod h1:2WAFYzt8rLYavcubpCoiym3iSCXiHdPB6DxtMkv2wyo=
14-
github.com/open-feature/go-sdk v1.16.0 h1:5NCHYv5slvNBIZhYXAzAufo0OI59OACZ5tczVqSE+Tg=
15-
github.com/open-feature/go-sdk v1.16.0/go.mod h1:EIF40QcoYT1VbQkMPy2ZJH4kvZeY+qGUXAorzSWgKSo=
1612
github.com/open-feature/go-sdk v1.17.0 h1:/OUBBw5d9D61JaNZZxb2Nnr5/EJrEpjtKCTY3rspJQk=
1713
github.com/open-feature/go-sdk v1.17.0/go.mod h1:lPxPSu1UnZ4E3dCxZi5gV3et2ACi8O8P+zsTGVsDZUw=
1814
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1915
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
20-
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
21-
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
16+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
17+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
2218
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
2319
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
24-
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
25-
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
2620
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
2721
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
28-
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
29-
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
3022
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
3123
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
32-
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
33-
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
3424
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
3525
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
36-
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
37-
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
3826
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
3927
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
40-
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
41-
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
4228
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
4329
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
4430
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
4531
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
46-
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
47-
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
4832
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
4933
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
50-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
51-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
5234
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
5335
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
54-
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
55-
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
36+
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
37+
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
5638
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
5739
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

hooks/open-telemetry/pkg/traces.go

Lines changed: 102 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,26 @@ package otel
33
import (
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.
1516
const 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.
1825
type 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

74150
type 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.
77155
func WithErrorStatusEnabled() Options {
78156
return func(h *traceHook) {
79-
h.setErrorStatus = true
80157
}
81158
}
82159

0 commit comments

Comments
 (0)