Skip to content
Original file line number Diff line number Diff line change
@@ -1,77 +1,4 @@
import json
from .metric import MetricsHook
from .trace import TracingHook

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.semconv.attributes.error_attributes import ERROR_TYPE

OTEL_EVENT_NAME = "feature_flag.evaluation"


class EventAttributes:
KEY = "feature_flag.key"
RESULT_VALUE = "feature_flag.result.value"
RESULT_VARIANT = "feature_flag.result.variant"
CONTEXT_ID = "feature_flag.context.id"
PROVIDER_NAME = "feature_flag.provider.name"
RESULT_REASON = "feature_flag.result.reason"
SET_ID = "feature_flag.set.id"
VERSION = "feature_flag.version"


class TracingHook(Hook):
def __init__(self, exclude_exceptions: bool = False):
self.exclude_exceptions = exclude_exceptions

def finally_after(
self,
hook_context: HookContext,
details: FlagEvaluationDetails,
hints: HookHints,
) -> None:
current_span = trace.get_current_span()

event_attributes = {
EventAttributes.KEY: details.flag_key,
EventAttributes.RESULT_VALUE: json.dumps(details.value),
EventAttributes.RESULT_REASON: str(
details.reason or Reason.UNKNOWN
).lower(),
}

if details.variant:
event_attributes[EventAttributes.RESULT_VARIANT] = details.variant

if details.reason == Reason.ERROR:
error_type = str(details.error_code or ErrorCode.GENERAL).lower()
event_attributes[ERROR_TYPE] = error_type
if details.error_message:
event_attributes["error.message"] = details.error_message

context = hook_context.evaluation_context
if context.targeting_key:
event_attributes[EventAttributes.CONTEXT_ID] = context.targeting_key

if hook_context.provider_metadata:
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: HookHints
) -> None:
if self.exclude_exceptions:
return
attributes = {
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)
__all__ = ["MetricsHook", "TracingHook"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Attributes:
OTEL_CONTEXT_ID = "feature_flag.context.id"
OTEL_EVENT_NAME = "feature_flag.evaluation"
OTEL_ERROR_TYPE = "error.type"
OTEL_ERROR_MESSAGE = "error.message"
OTEL_FLAG_KEY = "feature_flag.key"
OTEL_FLAG_VARIANT = "feature_flag.result.variant"
OTEL_PROVIDER_NAME = "feature_flag.provider.name"
OTEL_RESULT_VALUE = "feature_flag.result.value"
OTEL_RESULT_REASON = "feature_flag.result.reason"


class Metrics:
ACTIVE_COUNT = "feature_flag.evaluation.active_count"
SUCCESS_TOTAL = "feature_flag.evaluation.success_total"
REQUEST_TOTAL = "feature_flag.evaluation.request_total"
ERROR_TOTAL = "feature_flag.evaluation.error_total"
DURATION = "feature_flag.evaluation.duration"
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import time
import typing

from openfeature.flag_evaluation import FlagEvaluationDetails, FlagMetadata, Reason
from openfeature.hook import Hook, HookContext, HookHints
from opentelemetry import metrics
from opentelemetry.util.types import AttributeValue

from .constants import Attributes, Metrics


class MetricsHook(Hook):
def __init__(self, extra_attributes: typing.Optional[list[str]] = None) -> None:
self.extra_attributes = extra_attributes or []
meter: metrics.Meter = metrics.get_meter("openfeature.hooks.opentelemetry")
self.evaluation_active_count = meter.create_up_down_counter(
Metrics.ACTIVE_COUNT, "active flag evaluations"
)
self.evaluation_error_total = meter.create_counter(
Metrics.ERROR_TOTAL, "error flag evaluations"
)
self.evaluation_success_total = meter.create_counter(
Metrics.SUCCESS_TOTAL, "success flag evaluations"
)
self.evaluation_request_total = meter.create_counter(
Metrics.REQUEST_TOTAL, "request flag evaluations"
)
self.evaluation_duration = meter.create_histogram(
Metrics.DURATION, unit="s", description="duration of flag evaluations"
)

def before(self, hook_context: HookContext, hints: HookHints) -> None:
attributes: dict[str, AttributeValue] = {
Attributes.OTEL_FLAG_KEY: hook_context.flag_key,
}
if hook_context.provider_metadata:
attributes[Attributes.OTEL_PROVIDER_NAME] = (
hook_context.provider_metadata.name
)
hook_context.hook_data["start_time"] = time.perf_counter()
self.evaluation_active_count.add(1, attributes)
self.evaluation_request_total.add(1, attributes)

def after(
self,
hook_context: HookContext,
details: FlagEvaluationDetails,
hints: HookHints,
) -> None:
attributes: dict[str, AttributeValue] = {
Attributes.OTEL_FLAG_KEY: details.flag_key,
Attributes.OTEL_RESULT_REASON: str(
details.reason or Reason.UNKNOWN
).lower(),
}
if details.variant:
attributes[Attributes.OTEL_FLAG_VARIANT] = details.variant
if hook_context.provider_metadata:
attributes[Attributes.OTEL_PROVIDER_NAME] = (
hook_context.provider_metadata.name
)
attributes = attributes | get_extra_attributes(
self.extra_attributes, details.flag_metadata
)
self.evaluation_success_total.add(1, attributes)

def error(
self, hook_context: HookContext, exception: Exception, hints: HookHints
) -> None:
attributes: dict[str, AttributeValue] = {
Attributes.OTEL_FLAG_KEY: hook_context.flag_key,
Attributes.OTEL_ERROR_MESSAGE: str(exception).lower(),
}
if hook_context.provider_metadata:
attributes[Attributes.OTEL_PROVIDER_NAME] = (
hook_context.provider_metadata.name
)
self.evaluation_error_total.add(1, attributes)

def finally_after(
self,
hook_context: HookContext,
details: FlagEvaluationDetails,
hints: HookHints,
) -> None:
attributes: dict[str, AttributeValue] = {
Attributes.OTEL_FLAG_KEY: hook_context.flag_key,
}
if hook_context.provider_metadata:
attributes[Attributes.OTEL_PROVIDER_NAME] = (
hook_context.provider_metadata.name
)
self.evaluation_active_count.add(-1, attributes)
start_time = hook_context.hook_data.get("start_time")
if start_time is not None:
elapsed = time.perf_counter() - start_time
self.evaluation_duration.record(elapsed, attributes)


def get_extra_attributes(
extra_attributes: list[str], metadata: FlagMetadata
) -> dict[str, AttributeValue]:
attributes: dict[str, AttributeValue] = {}
for attribute in extra_attributes:
if (attr := metadata.get(attribute)) is not None:
attributes[attribute] = attr
return attributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import json

from openfeature.exception import ErrorCode
from openfeature.flag_evaluation import FlagEvaluationDetails, Reason
from openfeature.hook import Hook, HookContext, HookHints
from opentelemetry import trace

from .constants import Attributes


class TracingHook(Hook):
def __init__(self, exclude_exceptions: bool = False):
self.exclude_exceptions = exclude_exceptions

def finally_after(
self,
hook_context: HookContext,
details: FlagEvaluationDetails,
hints: HookHints,
) -> None:
current_span = trace.get_current_span()

event_attributes = {
Attributes.OTEL_FLAG_KEY: details.flag_key,
Attributes.OTEL_RESULT_VALUE: json.dumps(details.value),
Attributes.OTEL_RESULT_REASON: str(
details.reason or Reason.UNKNOWN
).lower(),
}

if details.variant:
event_attributes[Attributes.OTEL_FLAG_VARIANT] = details.variant

if details.reason == Reason.ERROR:
error_type = str(details.error_code or ErrorCode.GENERAL).lower()
event_attributes[Attributes.OTEL_ERROR_TYPE] = error_type
if details.error_message:
event_attributes[Attributes.OTEL_ERROR_MESSAGE] = details.error_message

context = hook_context.evaluation_context
if context.targeting_key:
event_attributes[Attributes.OTEL_CONTEXT_ID] = context.targeting_key

if hook_context.provider_metadata:
event_attributes[Attributes.OTEL_PROVIDER_NAME] = (
hook_context.provider_metadata.name
)

current_span.add_event(Attributes.OTEL_EVENT_NAME, event_attributes)

def error(
self, hook_context: HookContext, exception: Exception, hints: HookHints
) -> None:
if self.exclude_exceptions:
return
attributes = {
Attributes.OTEL_FLAG_KEY: hook_context.flag_key,
Attributes.OTEL_RESULT_VALUE: json.dumps(hook_context.default_value),
}
if hook_context.provider_metadata:
attributes[Attributes.OTEL_PROVIDER_NAME] = (
hook_context.provider_metadata.name
)
current_span = trace.get_current_span()
current_span.record_exception(exception, attributes)
Loading
Loading