feat: add OTel utility function (#451)
add OTel utility function Signed-off-by: gruebel <anton.gruebel@gmail.com> Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
This commit is contained in:
parent
613388ddde
commit
2d1ba85c93
|
|
@ -2,7 +2,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
import typing
|
import typing
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from enum import Enum
|
|
||||||
|
from openfeature._backports.strenum import StrEnum
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ErrorCode",
|
"ErrorCode",
|
||||||
|
|
@ -163,7 +164,7 @@ class InvalidContextError(OpenFeatureError):
|
||||||
super().__init__(ErrorCode.INVALID_CONTEXT, error_message)
|
super().__init__(ErrorCode.INVALID_CONTEXT, error_message)
|
||||||
|
|
||||||
|
|
||||||
class ErrorCode(Enum):
|
class ErrorCode(StrEnum):
|
||||||
PROVIDER_NOT_READY = "PROVIDER_NOT_READY"
|
PROVIDER_NOT_READY = "PROVIDER_NOT_READY"
|
||||||
PROVIDER_FATAL = "PROVIDER_FATAL"
|
PROVIDER_FATAL = "PROVIDER_FATAL"
|
||||||
FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
|
FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,9 @@ class Reason(StrEnum):
|
||||||
DEFAULT = "DEFAULT"
|
DEFAULT = "DEFAULT"
|
||||||
DISABLED = "DISABLED"
|
DISABLED = "DISABLED"
|
||||||
ERROR = "ERROR"
|
ERROR = "ERROR"
|
||||||
STATIC = "STATIC"
|
|
||||||
SPLIT = "SPLIT"
|
SPLIT = "SPLIT"
|
||||||
|
STATIC = "STATIC"
|
||||||
|
STALE = "STALE"
|
||||||
TARGETING_MATCH = "TARGETING_MATCH"
|
TARGETING_MATCH = "TARGETING_MATCH"
|
||||||
UNKNOWN = "UNKNOWN"
|
UNKNOWN = "UNKNOWN"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import typing
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from openfeature.exception import ErrorCode
|
||||||
|
from openfeature.flag_evaluation import FlagEvaluationDetails, Reason
|
||||||
|
from openfeature.hook import HookContext
|
||||||
|
from openfeature.telemetry.attributes import TelemetryAttribute
|
||||||
|
from openfeature.telemetry.body import TelemetryBodyField
|
||||||
|
from openfeature.telemetry.metadata import TelemetryFlagMetadata
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"EvaluationEvent",
|
||||||
|
"TelemetryAttribute",
|
||||||
|
"TelemetryBodyField",
|
||||||
|
"TelemetryFlagMetadata",
|
||||||
|
"create_evaluation_event",
|
||||||
|
]
|
||||||
|
|
||||||
|
FLAG_EVALUATION_EVENT_NAME = "feature_flag.evaluation"
|
||||||
|
|
||||||
|
T_co = typing.TypeVar("T_co", covariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EvaluationEvent(typing.Generic[T_co]):
|
||||||
|
name: str
|
||||||
|
attributes: Mapping[TelemetryAttribute, typing.Union[str, T_co]]
|
||||||
|
body: Mapping[TelemetryBodyField, T_co]
|
||||||
|
|
||||||
|
|
||||||
|
def create_evaluation_event(
|
||||||
|
hook_context: HookContext, details: FlagEvaluationDetails[T_co]
|
||||||
|
) -> EvaluationEvent[T_co]:
|
||||||
|
attributes = {
|
||||||
|
TelemetryAttribute.KEY: details.flag_key,
|
||||||
|
TelemetryAttribute.EVALUATION_REASON: (
|
||||||
|
details.reason or Reason.UNKNOWN
|
||||||
|
).lower(),
|
||||||
|
}
|
||||||
|
body = {}
|
||||||
|
|
||||||
|
if variant := details.variant:
|
||||||
|
attributes[TelemetryAttribute.VARIANT] = variant
|
||||||
|
else:
|
||||||
|
body[TelemetryBodyField.VALUE] = details.value
|
||||||
|
|
||||||
|
context_id = details.flag_metadata.get(
|
||||||
|
TelemetryFlagMetadata.CONTEXT_ID, hook_context.evaluation_context.targeting_key
|
||||||
|
)
|
||||||
|
if context_id:
|
||||||
|
attributes[TelemetryAttribute.CONTEXT_ID] = context_id
|
||||||
|
|
||||||
|
if set_id := details.flag_metadata.get(TelemetryFlagMetadata.FLAG_SET_ID):
|
||||||
|
attributes[TelemetryAttribute.SET_ID] = set_id
|
||||||
|
|
||||||
|
if version := details.flag_metadata.get(TelemetryFlagMetadata.VERSION):
|
||||||
|
attributes[TelemetryAttribute.VERSION] = version
|
||||||
|
|
||||||
|
if metadata := hook_context.provider_metadata:
|
||||||
|
attributes[TelemetryAttribute.PROVIDER_NAME] = metadata.name
|
||||||
|
|
||||||
|
if details.reason == Reason.ERROR:
|
||||||
|
attributes[TelemetryAttribute.ERROR_TYPE] = (
|
||||||
|
details.error_code or ErrorCode.GENERAL
|
||||||
|
).lower()
|
||||||
|
|
||||||
|
if err_msg := details.error_message:
|
||||||
|
attributes[TelemetryAttribute.EVALUATION_ERROR_MESSAGE] = err_msg
|
||||||
|
|
||||||
|
return EvaluationEvent(
|
||||||
|
name=FLAG_EVALUATION_EVENT_NAME,
|
||||||
|
attributes=attributes,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
from openfeature._backports.strenum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
class TelemetryAttribute(StrEnum):
|
||||||
|
"""
|
||||||
|
The attributes of an OpenTelemetry compliant event for flag evaluation.
|
||||||
|
|
||||||
|
See: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
|
||||||
|
"""
|
||||||
|
|
||||||
|
CONTEXT_ID = "feature_flag.context.id"
|
||||||
|
ERROR_TYPE = "error.type"
|
||||||
|
EVALUATION_ERROR_MESSAGE = "feature_flag.evaluation.error.message"
|
||||||
|
EVALUATION_REASON = "feature_flag.evaluation.reason"
|
||||||
|
KEY = "feature_flag.key"
|
||||||
|
PROVIDER_NAME = "feature_flag.provider_name"
|
||||||
|
SET_ID = "feature_flag.set.id"
|
||||||
|
VARIANT = "feature_flag.variant"
|
||||||
|
VERSION = "feature_flag.version"
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from openfeature._backports.strenum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
class TelemetryBodyField(StrEnum):
|
||||||
|
"""
|
||||||
|
OpenTelemetry event body fields.
|
||||||
|
|
||||||
|
See: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
|
||||||
|
"""
|
||||||
|
|
||||||
|
VALUE = "value"
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
from openfeature._backports.strenum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
class TelemetryFlagMetadata(StrEnum):
|
||||||
|
"""
|
||||||
|
Well-known flag metadata attributes for telemetry events.
|
||||||
|
|
||||||
|
See: https://openfeature.dev/specification/appendix-d/#flag-metadata
|
||||||
|
"""
|
||||||
|
|
||||||
|
CONTEXT_ID = "contextId"
|
||||||
|
FLAG_SET_ID = "flagSetId"
|
||||||
|
VERSION = "version"
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
from openfeature.evaluation_context import EvaluationContext
|
||||||
|
from openfeature.exception import ErrorCode
|
||||||
|
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType, Reason
|
||||||
|
from openfeature.hook import HookContext
|
||||||
|
from openfeature.provider import Metadata
|
||||||
|
from openfeature.telemetry import (
|
||||||
|
TelemetryAttribute,
|
||||||
|
TelemetryBodyField,
|
||||||
|
TelemetryFlagMetadata,
|
||||||
|
create_evaluation_event,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_evaluation_event():
|
||||||
|
# given
|
||||||
|
hook_context = HookContext(
|
||||||
|
flag_key="flag_key",
|
||||||
|
flag_type=FlagType.BOOLEAN,
|
||||||
|
default_value=True,
|
||||||
|
evaluation_context=EvaluationContext(),
|
||||||
|
provider_metadata=Metadata(name="test_provider"),
|
||||||
|
)
|
||||||
|
details = FlagEvaluationDetails(
|
||||||
|
flag_key=hook_context.flag_key,
|
||||||
|
value=False,
|
||||||
|
reason=Reason.CACHED,
|
||||||
|
)
|
||||||
|
|
||||||
|
# when
|
||||||
|
event = create_evaluation_event(hook_context=hook_context, details=details)
|
||||||
|
|
||||||
|
# then
|
||||||
|
assert event.name == "feature_flag.evaluation"
|
||||||
|
assert event.attributes[TelemetryAttribute.KEY] == "flag_key"
|
||||||
|
assert event.attributes[TelemetryAttribute.EVALUATION_REASON] == "cached"
|
||||||
|
assert event.attributes[TelemetryAttribute.PROVIDER_NAME] == "test_provider"
|
||||||
|
assert event.body[TelemetryBodyField.VALUE] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_evaluation_event_with_variant():
|
||||||
|
# given
|
||||||
|
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, EvaluationContext())
|
||||||
|
details = FlagEvaluationDetails(
|
||||||
|
flag_key=hook_context.flag_key,
|
||||||
|
value=True,
|
||||||
|
variant="true",
|
||||||
|
)
|
||||||
|
|
||||||
|
# when
|
||||||
|
event = create_evaluation_event(hook_context=hook_context, details=details)
|
||||||
|
|
||||||
|
# then
|
||||||
|
assert event.name == "feature_flag.evaluation"
|
||||||
|
assert event.attributes[TelemetryAttribute.KEY] == "flag_key"
|
||||||
|
assert event.attributes[TelemetryAttribute.VARIANT] == "true"
|
||||||
|
assert event.attributes[TelemetryAttribute.EVALUATION_REASON] == "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_evaluation_event_with_metadata():
|
||||||
|
# given
|
||||||
|
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, EvaluationContext())
|
||||||
|
details = FlagEvaluationDetails(
|
||||||
|
flag_key=hook_context.flag_key,
|
||||||
|
value=False,
|
||||||
|
flag_metadata={
|
||||||
|
TelemetryFlagMetadata.CONTEXT_ID: "5157782b-2203-4c80-a857-dbbd5e7761db",
|
||||||
|
TelemetryFlagMetadata.FLAG_SET_ID: "proj-1",
|
||||||
|
TelemetryFlagMetadata.VERSION: "v1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# when
|
||||||
|
event = create_evaluation_event(hook_context=hook_context, details=details)
|
||||||
|
|
||||||
|
# then
|
||||||
|
assert (
|
||||||
|
event.attributes[TelemetryAttribute.CONTEXT_ID]
|
||||||
|
== "5157782b-2203-4c80-a857-dbbd5e7761db"
|
||||||
|
)
|
||||||
|
assert event.attributes[TelemetryAttribute.SET_ID] == "proj-1"
|
||||||
|
assert event.attributes[TelemetryAttribute.VERSION] == "v1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_evaluation_event_with_error():
|
||||||
|
# given
|
||||||
|
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, EvaluationContext())
|
||||||
|
details = FlagEvaluationDetails(
|
||||||
|
flag_key=hook_context.flag_key,
|
||||||
|
value=False,
|
||||||
|
reason=Reason.ERROR,
|
||||||
|
error_code=ErrorCode.FLAG_NOT_FOUND,
|
||||||
|
error_message="flag error",
|
||||||
|
)
|
||||||
|
|
||||||
|
# when
|
||||||
|
event = create_evaluation_event(hook_context=hook_context, details=details)
|
||||||
|
|
||||||
|
# then
|
||||||
|
assert event.attributes[TelemetryAttribute.EVALUATION_REASON] == "error"
|
||||||
|
assert event.attributes[TelemetryAttribute.ERROR_TYPE] == "flag_not_found"
|
||||||
|
assert event.attributes[TelemetryAttribute.EVALUATION_ERROR_MESSAGE] == "flag error"
|
||||||
Loading…
Reference in New Issue