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
|
||||
from collections.abc import Mapping
|
||||
from enum import Enum
|
||||
|
||||
from openfeature._backports.strenum import StrEnum
|
||||
|
||||
__all__ = [
|
||||
"ErrorCode",
|
||||
|
|
@ -163,7 +164,7 @@ class InvalidContextError(OpenFeatureError):
|
|||
super().__init__(ErrorCode.INVALID_CONTEXT, error_message)
|
||||
|
||||
|
||||
class ErrorCode(Enum):
|
||||
class ErrorCode(StrEnum):
|
||||
PROVIDER_NOT_READY = "PROVIDER_NOT_READY"
|
||||
PROVIDER_FATAL = "PROVIDER_FATAL"
|
||||
FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@ class Reason(StrEnum):
|
|||
DEFAULT = "DEFAULT"
|
||||
DISABLED = "DISABLED"
|
||||
ERROR = "ERROR"
|
||||
STATIC = "STATIC"
|
||||
SPLIT = "SPLIT"
|
||||
STATIC = "STATIC"
|
||||
STALE = "STALE"
|
||||
TARGETING_MATCH = "TARGETING_MATCH"
|
||||
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