Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,17 @@ client.get_boolean_flag("my-flag", False, flag_evaluation_options=options)

The OpenFeature SDK logs to the `openfeature` logger using the `logging` package from the Python Standard Library.

#### Logging Hook

The Python SDK includes a `LoggingHook`, which logs detailed information at key points during flag evaluation, using the `logging` package. This hook can be particularly helpful for troubleshooting and debugging; simply attach it at the global, client or invocation level and ensure your log level is set to "debug".

```python
from openfeature import api
from openfeature.hook import LoggingHook

api.add_hooks([LoggingHook()])
```

### Domains

Clients can be assigned to a domain.
Expand Down
141 changes: 3 additions & 138 deletions openfeature/hook/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
from __future__ import annotations

import typing
from collections.abc import Mapping, MutableMapping, Sequence
from datetime import datetime
from enum import Enum

from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType, FlagValueType

if typing.TYPE_CHECKING:
from openfeature.client import ClientMetadata
from openfeature.provider.metadata import Metadata
from openfeature.hook.hook import Hook, HookContext, HookData, HookHints, HookType
from openfeature.hook.logging_hook import LoggingHook

__all__ = [
"Hook",
"HookContext",
"HookData",
"HookHints",
"HookType",
"LoggingHook",
"add_hooks",
"clear_hooks",
"get_hooks",
Expand All @@ -26,131 +16,6 @@
_hooks: list[Hook] = []


# https://openfeature.dev/specification/sections/hooks/#requirement-461
HookData = MutableMapping[str, typing.Any]


class HookType(Enum):
BEFORE = "before"
AFTER = "after"
FINALLY_AFTER = "finally_after"
ERROR = "error"


class HookContext:
def __init__( # noqa: PLR0913
self,
flag_key: str,
flag_type: FlagType,
default_value: FlagValueType,
evaluation_context: EvaluationContext,
client_metadata: ClientMetadata | None = None,
provider_metadata: Metadata | None = None,
hook_data: HookData | None = None,
):
self.flag_key = flag_key
self.flag_type = flag_type
self.default_value = default_value
self.evaluation_context = evaluation_context
self.client_metadata = client_metadata
self.provider_metadata = provider_metadata
self.hook_data = hook_data or {}

def __setattr__(self, key: str, value: typing.Any) -> None:
if hasattr(self, key) and key in (
"flag_key",
"flag_type",
"default_value",
"client_metadata",
"provider_metadata",
):
raise AttributeError(f"Attribute {key!r} is immutable")
super().__setattr__(key, value)


# https://openfeature.dev/specification/sections/hooks/#requirement-421
HookHintValue: typing.TypeAlias = (
bool
| int
| float
| str
| datetime
| Sequence["HookHintValue"]
| Mapping[str, "HookHintValue"]
)

HookHints = Mapping[str, HookHintValue]


class Hook:
def before(
self, hook_context: HookContext, hints: HookHints
) -> EvaluationContext | None:
"""
Runs before flag is resolved.

:param hook_context: Information about the particular flag evaluation
:param hints: An immutable mapping of data for users to
communicate to the hooks.
:return: An EvaluationContext. It will be merged with the
EvaluationContext instances from other hooks, the client and API.
"""
return None

def after(
self,
hook_context: HookContext,
details: FlagEvaluationDetails[FlagValueType],
hints: HookHints,
) -> None:
"""
Runs after a flag is resolved.

:param hook_context: Information about the particular flag evaluation
:param details: Information about how the flag was resolved,
including any resolved values.
:param hints: A mapping of data for users to communicate to the hooks.
"""
pass

def error(
self, hook_context: HookContext, exception: Exception, hints: HookHints
) -> None:
"""
Run when evaluation encounters an error. Errors thrown will be swallowed.

:param hook_context: Information about the particular flag evaluation
:param exception: The exception that was thrown
:param hints: A mapping of data for users to communicate to the hooks.
"""
pass

def finally_after(
self,
hook_context: HookContext,
details: FlagEvaluationDetails[FlagValueType],
hints: HookHints,
) -> None:
"""
Run after flag evaluation, including any error processing.
This will always run. Errors will be swallowed.

:param hook_context: Information about the particular flag evaluation
:param hints: A mapping of data for users to communicate to the hooks.
"""
pass

def supports_flag_value_type(self, flag_type: FlagType) -> bool:
"""
Check to see if the hook supports the particular flag type.

:param flag_type: particular type of the flag
:return: a boolean containing whether the flag type is supported (True)
or not (False)
"""
return True


def add_hooks(hooks: list[Hook]) -> None:
global _hooks
_hooks = _hooks + hooks
Expand Down
137 changes: 137 additions & 0 deletions openfeature/hook/hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from __future__ import annotations

import typing
from collections.abc import Mapping, MutableMapping, Sequence
from datetime import datetime
from enum import Enum

from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType, FlagValueType

if typing.TYPE_CHECKING:
from openfeature.client import ClientMetadata
from openfeature.provider.metadata import Metadata

# https://openfeature.dev/specification/sections/hooks/#requirement-461
HookData = MutableMapping[str, typing.Any]


class HookType(Enum):
BEFORE = "before"
AFTER = "after"
FINALLY_AFTER = "finally_after"
ERROR = "error"


class HookContext:
def __init__( # noqa: PLR0913
self,
flag_key: str,
flag_type: FlagType,
default_value: FlagValueType,
evaluation_context: EvaluationContext,
client_metadata: ClientMetadata | None = None,
provider_metadata: Metadata | None = None,
hook_data: HookData | None = None,
):
self.flag_key = flag_key
self.flag_type = flag_type
self.default_value = default_value
self.evaluation_context = evaluation_context
self.client_metadata = client_metadata
self.provider_metadata = provider_metadata
self.hook_data = hook_data or {}

def __setattr__(self, key: str, value: typing.Any) -> None:
if hasattr(self, key) and key in (
"flag_key",
"flag_type",
"default_value",
"client_metadata",
"provider_metadata",
):
raise AttributeError(f"Attribute {key!r} is immutable")
super().__setattr__(key, value)


# https://openfeature.dev/specification/sections/hooks/#requirement-421
HookHintValue: typing.TypeAlias = (
bool
| int
| float
| str
| datetime
| Sequence["HookHintValue"]
| Mapping[str, "HookHintValue"]
)

HookHints = Mapping[str, HookHintValue]


class Hook:
def before(
self, hook_context: HookContext, hints: HookHints
) -> EvaluationContext | None:
"""
Runs before flag is resolved.

:param hook_context: Information about the particular flag evaluation
:param hints: An immutable mapping of data for users to
communicate to the hooks.
:return: An EvaluationContext. It will be merged with the
EvaluationContext instances from other hooks, the client and API.
"""
return None

def after(
self,
hook_context: HookContext,
details: FlagEvaluationDetails[FlagValueType],
hints: HookHints,
) -> None:
"""
Runs after a flag is resolved.

:param hook_context: Information about the particular flag evaluation
:param details: Information about how the flag was resolved,
including any resolved values.
:param hints: A mapping of data for users to communicate to the hooks.
"""
pass

def error(
self, hook_context: HookContext, exception: Exception, hints: HookHints
) -> None:
"""
Run when evaluation encounters an error. Errors thrown will be swallowed.

:param hook_context: Information about the particular flag evaluation
:param exception: The exception that was thrown
:param hints: A mapping of data for users to communicate to the hooks.
"""
pass

def finally_after(
self,
hook_context: HookContext,
details: FlagEvaluationDetails[FlagValueType],
hints: HookHints,
) -> None:
"""
Run after flag evaluation, including any error processing.
This will always run. Errors will be swallowed.

:param hook_context: Information about the particular flag evaluation
:param hints: A mapping of data for users to communicate to the hooks.
"""
pass

def supports_flag_value_type(self, flag_type: FlagType) -> bool:
"""
Check to see if the hook supports the particular flag type.

:param flag_type: particular type of the flag
:return: a boolean containing whether the flag type is supported (True)
or not (False)
"""
return True
72 changes: 72 additions & 0 deletions openfeature/hook/logging_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import json
import logging

from openfeature.evaluation_context import EvaluationContext
from openfeature.exception import ErrorCode, OpenFeatureError
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagValueType
from openfeature.hook.hook import Hook, HookContext, HookHints


class LoggingHook(Hook):
def __init__(
self,
include_evaluation_context: bool = False,
logger: logging.Logger | None = None,
):
self.logger = logger or logging.getLogger("openfeature")
self.include_evaluation_context = include_evaluation_context

def _build_args(self, hook_context: HookContext) -> dict:
args = {
"domain": hook_context.client_metadata.domain
if hook_context.client_metadata
else None,
"provider_name": hook_context.provider_metadata.name
if hook_context.provider_metadata
else None,
"flag_key": hook_context.flag_key,
"default_value": hook_context.default_value,
}
if self.include_evaluation_context:
args["evaluation_context"] = json.dumps(
{
"targeting_key": hook_context.evaluation_context.targeting_key,
"attributes": hook_context.evaluation_context.attributes,
},
default=str,
)
return args

def before(
self, hook_context: HookContext, hints: HookHints
) -> EvaluationContext | None:
args = self._build_args(hook_context)
args["stage"] = "before"
self.logger.debug("Before stage %s", args)
return None

def after(
self,
hook_context: HookContext,
details: FlagEvaluationDetails[FlagValueType],
hints: HookHints,
) -> None:
args = self._build_args(hook_context)
args["stage"] = "after"
args["reason"] = details.reason
args["variant"] = details.variant
args["value"] = details.value
self.logger.debug("After stage %s", args)

def error(
self, hook_context: HookContext, exception: Exception, hints: HookHints
) -> None:
args = self._build_args(hook_context)
args["stage"] = "error"
args["error_code"] = (
exception.error_code
if isinstance(exception, OpenFeatureError)
else ErrorCode.GENERAL
)
args["error_message"] = str(exception)
self.logger.error("Error stage %s", args)
Loading
Loading