Skip to content

Commit 6b5fe18

Browse files
committed
feat: update opentelemetry hook to latest version of semantic conventions
Signed-off-by: Federico Bond <[email protected]>
1 parent 223a903 commit 6b5fe18

File tree

4 files changed

+102
-25
lines changed

4 files changed

+102
-25
lines changed

hooks/openfeature-hooks-opentelemetry/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ classifiers = [
1616
]
1717
keywords = []
1818
dependencies = [
19-
"openfeature-sdk>=0.6.0",
19+
"openfeature-sdk>=0.8.4",
2020
"opentelemetry-api",
21+
"opentelemetry-semantic-conventions>=0.50b0",
2122
]
2223
requires-python = ">=3.9"
2324

hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import json
22

3-
from openfeature.flag_evaluation import FlagEvaluationDetails
3+
from openfeature.exception import ErrorCode
4+
from openfeature.flag_evaluation import FlagEvaluationDetails, Reason
45
from openfeature.hook import Hook, HookContext, HookHints
56
from opentelemetry import trace
7+
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
68

7-
OTEL_EVENT_NAME = "feature_flag"
9+
OTEL_EVENT_NAME = "feature_flag.evaluation"
810

911

1012
class EventAttributes:
11-
FLAG_KEY = f"{OTEL_EVENT_NAME}.key"
12-
FLAG_VARIANT = f"{OTEL_EVENT_NAME}.variant"
13-
PROVIDER_NAME = f"{OTEL_EVENT_NAME}.provider_name"
13+
KEY = "feature_flag.key"
14+
RESULT_VALUE = "feature_flag.result.value"
15+
RESULT_VARIANT = "feature_flag.result.variant"
16+
CONTEXT_ID = "feature_flag.context.id"
17+
PROVIDER_NAME = "feature_flag.provider.name"
18+
RESULT_REASON = "feature_flag.result.reason"
19+
SET_ID = "feature_flag.set.id"
20+
VERSION = "feature_flag.version"
1421

1522

1623
class TracingHook(Hook):
@@ -22,18 +29,27 @@ def after(
2229
) -> None:
2330
current_span = trace.get_current_span()
2431

25-
variant = details.variant
26-
if variant is None:
27-
if isinstance(details.value, str):
28-
variant = str(details.value)
29-
else:
30-
variant = json.dumps(details.value)
31-
3232
event_attributes = {
33-
EventAttributes.FLAG_KEY: details.flag_key,
34-
EventAttributes.FLAG_VARIANT: variant,
33+
EventAttributes.KEY: details.flag_key,
34+
EventAttributes.RESULT_VALUE: json.dumps(details.value),
35+
EventAttributes.RESULT_REASON: str(
36+
details.reason or Reason.UNKNOWN
37+
).lower(),
3538
}
3639

40+
if details.variant:
41+
event_attributes[EventAttributes.RESULT_VARIANT] = details.variant
42+
43+
if details.reason == Reason.ERROR:
44+
error_type = str(details.error_code or ErrorCode.GENERAL).lower()
45+
event_attributes[ERROR_TYPE] = error_type
46+
if details.error_message:
47+
event_attributes["error.message"] = details.error_message
48+
49+
context = hook_context.evaluation_context
50+
if context.targeting_key:
51+
event_attributes[EventAttributes.CONTEXT_ID] = context.targeting_key
52+
3753
if hook_context.provider_metadata:
3854
event_attributes[EventAttributes.PROVIDER_NAME] = (
3955
hook_context.provider_metadata.name

hooks/openfeature-hooks-opentelemetry/tests/test_otel.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,32 @@
66

77
from openfeature.contrib.hook.opentelemetry import TracingHook
88
from openfeature.evaluation_context import EvaluationContext
9-
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType
9+
from openfeature.exception import ErrorCode
10+
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType, Reason
1011
from openfeature.hook import HookContext
12+
from openfeature.provider.metadata import Metadata
1113

1214

1315
@pytest.fixture
1416
def mock_get_current_span(monkeypatch):
1517
monkeypatch.setattr(trace, "get_current_span", Mock())
1618

1719

18-
def test_before(mock_get_current_span):
20+
def test_after(mock_get_current_span):
1921
# Given
2022
hook = TracingHook()
2123
hook_context = HookContext(
2224
flag_key="flag_key",
2325
flag_type=FlagType.BOOLEAN,
2426
default_value=False,
25-
evaluation_context=EvaluationContext(),
27+
evaluation_context=EvaluationContext("123"),
28+
provider_metadata=Metadata(name="test-provider"),
2629
)
2730
details = FlagEvaluationDetails(
2831
flag_key="flag_key",
2932
value=True,
3033
variant="enabled",
31-
reason=None,
34+
reason=Reason.TARGETING_MATCH,
3235
error_code=None,
3336
error_message=None,
3437
)
@@ -41,10 +44,52 @@ def test_before(mock_get_current_span):
4144

4245
# Then
4346
mock_span.add_event.assert_called_once_with(
44-
"feature_flag",
47+
"feature_flag.evaluation",
48+
{
49+
"feature_flag.key": "flag_key",
50+
"feature_flag.result.value": "true",
51+
"feature_flag.result.variant": "enabled",
52+
"feature_flag.result.reason": "targeting_match",
53+
"feature_flag.context.id": "123",
54+
"feature_flag.provider.name": "test-provider",
55+
},
56+
)
57+
58+
59+
def test_after_evaluation_error(mock_get_current_span):
60+
# Given
61+
hook = TracingHook()
62+
hook_context = HookContext(
63+
flag_key="flag_key",
64+
flag_type=FlagType.BOOLEAN,
65+
default_value=False,
66+
evaluation_context=EvaluationContext(),
67+
provider_metadata=None,
68+
)
69+
details = FlagEvaluationDetails(
70+
flag_key="flag_key",
71+
value=False,
72+
variant=None,
73+
reason=Reason.ERROR,
74+
error_code=ErrorCode.FLAG_NOT_FOUND,
75+
error_message="Flag not found: flag_key",
76+
)
77+
78+
mock_span = Mock(spec=Span)
79+
trace.get_current_span.return_value = mock_span
80+
81+
# When
82+
hook.after(hook_context, details, hints={})
83+
84+
# Then
85+
mock_span.add_event.assert_called_once_with(
86+
"feature_flag.evaluation",
4587
{
4688
"feature_flag.key": "flag_key",
47-
"feature_flag.variant": "enabled",
89+
"feature_flag.result.value": "false",
90+
"feature_flag.result.reason": "error",
91+
"error.type": "flag_not_found",
92+
"error.message": "Flag not found: flag_key",
4893
},
4994
)
5095

uv.lock

Lines changed: 19 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)