From 5a917d1fd0ba2c8a68a41067d5bef7c8fd3ea707 Mon Sep 17 00:00:00 2001 From: Danju Visvanathan Date: Sun, 8 Feb 2026 03:51:01 +1100 Subject: [PATCH 1/5] refactor: move after logic to finally_after so evaluation span is created on failure Signed-off-by: Danju Visvanathan --- .../openfeature/contrib/hook/opentelemetry/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py b/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py index 892e6245..eaf1aa18 100644 --- a/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py +++ b/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py @@ -3,7 +3,7 @@ from openfeature.exception import ErrorCode from openfeature.flag_evaluation import FlagEvaluationDetails, Reason from openfeature.hook import Hook, HookContext, HookHints -from opentelemetry import trace +from opentelemetry import metrics, trace from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE OTEL_EVENT_NAME = "feature_flag.evaluation" @@ -21,7 +21,7 @@ class EventAttributes: class TracingHook(Hook): - def after( + def finally_after( self, hook_context: HookContext, details: FlagEvaluationDetails, @@ -60,5 +60,9 @@ def after( def error( self, hook_context: HookContext, exception: Exception, hints: HookHints ) -> None: + attributes = { + EventAttributes.KEY: hook_context.flag_key, + EventAttributes.RESULT_VALUE: json.dumps(hook_context.default_value), + } current_span = trace.get_current_span() - current_span.record_exception(exception) + current_span.record_exception(exception, attributes) From eed03a071ef71fd55e5390ca04c3d2d6ee43121c Mon Sep 17 00:00:00 2001 From: Danju Visvanathan Date: Mon, 9 Feb 2026 00:14:28 +1100 Subject: [PATCH 2/5] feat: optionally disable exceptions on evaluation errors Signed-off-by: Danju Visvanathan --- .../contrib/hook/opentelemetry/__init__.py | 7 +++- .../tests/test_otel.py | 33 ++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py b/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py index eaf1aa18..4819be29 100644 --- a/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py +++ b/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py @@ -3,7 +3,7 @@ from openfeature.exception import ErrorCode from openfeature.flag_evaluation import FlagEvaluationDetails, Reason from openfeature.hook import Hook, HookContext, HookHints -from opentelemetry import metrics, trace +from opentelemetry import trace from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE OTEL_EVENT_NAME = "feature_flag.evaluation" @@ -21,6 +21,9 @@ class EventAttributes: class TracingHook(Hook): + def __init__(self, exclude_exceptions: bool = False): + self.exclude_exceptions = exclude_exceptions + def finally_after( self, hook_context: HookContext, @@ -60,6 +63,8 @@ def finally_after( def error( self, hook_context: HookContext, exception: Exception, hints: HookHints ) -> None: + if self.exclude_exceptions: + return attributes = { EventAttributes.KEY: hook_context.flag_key, EventAttributes.RESULT_VALUE: json.dumps(hook_context.default_value), diff --git a/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py b/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py index f8b74385..f5f7ca34 100644 --- a/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py +++ b/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py @@ -12,12 +12,14 @@ from openfeature.provider.metadata import Metadata + + @pytest.fixture def mock_get_current_span(monkeypatch): monkeypatch.setattr(trace, "get_current_span", Mock()) -def test_after(mock_get_current_span): +def test_finally_after(mock_get_current_span): # Given hook = TracingHook() hook_context = HookContext( @@ -40,7 +42,7 @@ def test_after(mock_get_current_span): trace.get_current_span.return_value = mock_span # When - hook.after(hook_context, details, hints={}) + hook.finally_after(hook_context, details, hints={}) # Then mock_span.add_event.assert_called_once_with( @@ -79,7 +81,7 @@ def test_after_evaluation_error(mock_get_current_span): trace.get_current_span.return_value = mock_span # When - hook.after(hook_context, details, hints={}) + hook.finally_after(hook_context, details, hints={}) # Then mock_span.add_event.assert_called_once_with( @@ -104,6 +106,10 @@ def test_error(mock_get_current_span): evaluation_context=EvaluationContext(), ) exception = Exception() + attributes = { + "feature_flag.key": "flag_key", + "feature_flag.result.value": "false", + } mock_span = Mock(spec=Span) trace.get_current_span.return_value = mock_span @@ -112,4 +118,23 @@ def test_error(mock_get_current_span): hook.error(hook_context, exception, hints={}) # Then - mock_span.record_exception.assert_called_once_with(exception) + mock_span.record_exception.assert_called_once_with(exception, attributes) + +def test_error_exclude_exceptions(mock_get_current_span): + # Given + hook = TracingHook(exclude_exceptions=True) + hook_context = HookContext( + flag_key="flag_key", + flag_type=FlagType.BOOLEAN, + default_value=False, + evaluation_context=EvaluationContext(), + ) + exception = Exception() + + mock_span = Mock(spec=Span) + trace.get_current_span.return_value = mock_span + + # When + hook.error(hook_context, exception, hints={}) + + mock_span.record_exception.assert_not_called() From 081ea6bd98410fc759ec49ce36d207183972f288 Mon Sep 17 00:00:00 2001 From: Danju Visvanathan Date: Thu, 12 Feb 2026 10:01:57 +1100 Subject: [PATCH 3/5] fix: add provider metadata to hook exception Signed-off-by: Danju Visvanathan --- .../contrib/hook/opentelemetry/__init__.py | 2 ++ .../tests/test_otel.py | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py b/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py index 4819be29..9ee1b05e 100644 --- a/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py +++ b/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py @@ -69,5 +69,7 @@ def error( EventAttributes.KEY: hook_context.flag_key, EventAttributes.RESULT_VALUE: json.dumps(hook_context.default_value), } + if hook_context.provider_metadata: + attributes[EventAttributes.PROVIDER_NAME] = hook_context.provider_metadata.name current_span = trace.get_current_span() current_span.record_exception(exception, attributes) diff --git a/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py b/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py index f5f7ca34..4c692df9 100644 --- a/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py +++ b/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py @@ -104,11 +104,13 @@ def test_error(mock_get_current_span): flag_type=FlagType.BOOLEAN, default_value=False, evaluation_context=EvaluationContext(), + provider_metadata=Metadata(name="test-provider"), ) exception = Exception() attributes = { "feature_flag.key": "flag_key", "feature_flag.result.value": "false", + "feature_flag.provider.name": "test-provider", } mock_span = Mock(spec=Span) @@ -138,3 +140,26 @@ def test_error_exclude_exceptions(mock_get_current_span): hook.error(hook_context, exception, hints={}) mock_span.record_exception.assert_not_called() + + +def test_error_no_provider_metadata(mock_get_current_span): + # Given + hook = TracingHook() + hook_context = HookContext( + flag_key="flag_key", + flag_type=FlagType.BOOLEAN, + default_value=False, + evaluation_context=EvaluationContext(), + ) + exception = Exception() + attributes = { + "feature_flag.key": "flag_key", + "feature_flag.result.value": "false", + } + + mock_span = Mock(spec=Span) + trace.get_current_span.return_value = mock_span + + # When + hook.error(hook_context, exception, hints={}) + mock_span.record_exception.assert_called_once_with(exception, attributes) \ No newline at end of file From c021302429e20d441e16144561c3a0b7fa05e116 Mon Sep 17 00:00:00 2001 From: Danju Visvanathan Date: Thu, 12 Feb 2026 10:04:47 +1100 Subject: [PATCH 4/5] chore: given/when/then comments Signed-off-by: Danju Visvanathan --- hooks/openfeature-hooks-opentelemetry/tests/test_otel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py b/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py index 4c692df9..da2ba996 100644 --- a/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py +++ b/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py @@ -138,7 +138,7 @@ def test_error_exclude_exceptions(mock_get_current_span): # When hook.error(hook_context, exception, hints={}) - + # Then mock_span.record_exception.assert_not_called() @@ -162,4 +162,5 @@ def test_error_no_provider_metadata(mock_get_current_span): # When hook.error(hook_context, exception, hints={}) + # Then mock_span.record_exception.assert_called_once_with(exception, attributes) \ No newline at end of file From 165d3e740c17598ef06f67fa60d3ba0f0ca011ce Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 12 Feb 2026 15:18:31 -0500 Subject: [PATCH 5/5] fixup: formatting Signed-off-by: Todd Baert --- .../src/openfeature/contrib/hook/opentelemetry/__init__.py | 4 +++- hooks/openfeature-hooks-opentelemetry/tests/test_otel.py | 5 ++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py b/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py index 9ee1b05e..56799f6c 100644 --- a/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py +++ b/hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/__init__.py @@ -70,6 +70,8 @@ def error( EventAttributes.RESULT_VALUE: json.dumps(hook_context.default_value), } if hook_context.provider_metadata: - attributes[EventAttributes.PROVIDER_NAME] = hook_context.provider_metadata.name + attributes[EventAttributes.PROVIDER_NAME] = ( + hook_context.provider_metadata.name + ) current_span = trace.get_current_span() current_span.record_exception(exception, attributes) diff --git a/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py b/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py index da2ba996..b0279f47 100644 --- a/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py +++ b/hooks/openfeature-hooks-opentelemetry/tests/test_otel.py @@ -12,8 +12,6 @@ from openfeature.provider.metadata import Metadata - - @pytest.fixture def mock_get_current_span(monkeypatch): monkeypatch.setattr(trace, "get_current_span", Mock()) @@ -122,6 +120,7 @@ def test_error(mock_get_current_span): # Then mock_span.record_exception.assert_called_once_with(exception, attributes) + def test_error_exclude_exceptions(mock_get_current_span): # Given hook = TracingHook(exclude_exceptions=True) @@ -163,4 +162,4 @@ def test_error_no_provider_metadata(mock_get_current_span): # When hook.error(hook_context, exception, hints={}) # Then - mock_span.record_exception.assert_called_once_with(exception, attributes) \ No newline at end of file + mock_span.record_exception.assert_called_once_with(exception, attributes)