Skip to content

Commit

Permalink
feat: implement OpenTelemetry traces hook
Browse files Browse the repository at this point in the history
  • Loading branch information
federicobond committed Jul 18, 2023
1 parent 1c52d12 commit febd150
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 1 deletion.
45 changes: 45 additions & 0 deletions open_feature_contrib/hooks/otel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import json

from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
from open_feature.hooks.hook import Hook
from open_feature.hooks.hook_context import HookContext
from opentelemetry import trace


OTEL_EVENT_NAME = "feature_flag"


class EventAttributes:
FLAG_KEY = f"{OTEL_EVENT_NAME}.key"
FLAG_VARIANT = f"{OTEL_EVENT_NAME}.variant"
PROVIDER_NAME = f"{OTEL_EVENT_NAME}.provider_name"


class OTelTracesHook(Hook):
def after(
self, hook_context: HookContext, details: FlagEvaluationDetails, hints: dict
):
current_span = trace.get_current_span()

variant = details.variant
if variant is None:
if isinstance(details.value, str):

Check warning on line 26 in open_feature_contrib/hooks/otel.py

View check run for this annotation

Codecov / codecov/patch

open_feature_contrib/hooks/otel.py#L25-L26

Added lines #L25 - L26 were not covered by tests
variant = str(details.value)
else:

Check warning on line 28 in open_feature_contrib/hooks/otel.py

View check run for this annotation

Codecov / codecov/patch

open_feature_contrib/hooks/otel.py#L28

Added line #L28 was not covered by tests
variant = json.dumps(details.value)

event_attributes = {
EventAttributes.FLAG_KEY: details.flag_key,
EventAttributes.FLAG_VARIANT: variant,
}

if hook_context.provider_metadata:

Check warning on line 36 in open_feature_contrib/hooks/otel.py

View check run for this annotation

Codecov / codecov/patch

open_feature_contrib/hooks/otel.py#L36

Added line #L36 was not covered by tests
event_attributes[
EventAttributes.PROVIDER_NAME
] = hook_context.provider_metadata.name

current_span.add_event(OTEL_EVENT_NAME, event_attributes)

def error(self, hook_context: HookContext, exception: Exception, hints: dict):
current_span = trace.get_current_span()
current_span.record_exception(exception)
1 change: 1 addition & 0 deletions requirements-dev.in
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ flake8
pytest-mock
coverage
openfeature-sdk
opentelemetry-api
12 changes: 11 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ click==8.1.3
# pip-tools
coverage==7.2.7
# via -r requirements-dev.in
deprecated==1.2.14
# via opentelemetry-api
dill==0.3.4
# via pylint
distlib==0.3.4
Expand All @@ -28,6 +30,8 @@ flake8==4.0.1
# via -r requirements-dev.in
identify==2.5.0
# via pre-commit
importlib-metadata==6.8.0
# via opentelemetry-api
iniconfig==1.1.1
# via pytest
isort==5.10.1
Expand All @@ -44,6 +48,8 @@ nodeenv==1.6.0
# via pre-commit
openfeature-sdk==0.0.9
# via -r requirements-dev.in
opentelemetry-api==1.19.0
# via -r requirements-dev.in
packaging==21.3
# via pytest
pathspec==0.9.0
Expand Down Expand Up @@ -92,7 +98,11 @@ virtualenv==20.14.1
wheel==0.37.1
# via pip-tools
wrapt==1.14.1
# via astroid
# via
# astroid
# deprecated
zipp==3.16.2
# via importlib-metadata

# The following packages are considered to be unsafe in a requirements file:
# pip
Expand Down
Empty file added tests/hooks/__init__.py
Empty file.
70 changes: 70 additions & 0 deletions tests/hooks/test_otel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from unittest.mock import Mock

import pytest
from open_feature.hooks.hook_context import HookContext
from open_feature.flag_evaluation.flag_type import FlagType
from open_feature.evaluation_context.evaluation_context import EvaluationContext
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
from open_feature_contrib.hooks.otel import OTelTracesHook
from opentelemetry import trace
from opentelemetry.trace import Span


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


def test_before(mock_get_current_span):
# Given
hook = OTelTracesHook()
hook_context = HookContext(
flag_key="flag_key",
flag_type=FlagType.BOOLEAN,
default_value=False,
evaluation_context=EvaluationContext(),
)
details = FlagEvaluationDetails(
flag_key="flag_key",
value=True,
variant="enabled",
reason=None,
error_code=None,
error_message=None,
)

mock_span = Mock(spec=Span)
trace.get_current_span.return_value = mock_span

# When
hook.after(hook_context, details, hints={})

# Then
mock_span.add_event.assert_called_once_with(
"feature_flag",
{
"feature_flag.key": "flag_key",
"feature_flag.variant": "enabled",
},
)


def test_error(mock_get_current_span):
# Given
hook = OTelTracesHook()
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={})

# Then
mock_span.record_exception.assert_called_once_with(exception)

0 comments on commit febd150

Please sign in to comment.