Skip to content

Commit fb8f888

Browse files
committed
feat: add histogram to record evaluation duration
Signed-off-by: Danju Visvanathan <danju.visvanathan@gmail.com>
1 parent 1a86d27 commit fb8f888

3 files changed

Lines changed: 45 additions & 0 deletions

File tree

hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ class Metrics:
1717
SUCCESS_TOTAL = "feature_flag.evaluation.success_total"
1818
REQUEST_TOTAL = "feature_flag.evaluation.request_total"
1919
ERROR_TOTAL = "feature_flag.evaluation.error_total"
20+
DURATION = "feature_flag.evaluation.duration"

hooks/openfeature-hooks-opentelemetry/src/openfeature/contrib/hook/opentelemetry/metric.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import time
12
import typing
23

34
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagMetadata, Reason
@@ -24,6 +25,9 @@ def __init__(self, extra_attributes: typing.Optional[list[str]] = None) -> None:
2425
self.evaluation_request_total = meter.create_counter(
2526
Metrics.REQUEST_TOTAL, "request flag evaluations"
2627
)
28+
self.evaluation_duration = meter.create_histogram(
29+
Metrics.DURATION, unit="s", description="duration of flag evaluations"
30+
)
2731

2832
def before(self, hook_context: HookContext, hints: HookHints) -> None:
2933
attributes: dict[str, AttributeValue] = {
@@ -33,6 +37,7 @@ def before(self, hook_context: HookContext, hints: HookHints) -> None:
3337
attributes[Attributes.OTEL_PROVIDER_NAME] = (
3438
hook_context.provider_metadata.name
3539
)
40+
hook_context.hook_data["start_time"] = time.perf_counter()
3641
self.evaluation_active_count.add(1, attributes)
3742
self.evaluation_request_total.add(1, attributes)
3843

@@ -86,6 +91,10 @@ def finally_after(
8691
hook_context.provider_metadata.name
8792
)
8893
self.evaluation_active_count.add(-1, attributes)
94+
start_time = hook_context.hook_data.get("start_time")
95+
if start_time is not None:
96+
elapsed = time.perf_counter() - start_time
97+
self.evaluation_duration.record(elapsed, attributes)
8998

9099

91100
def get_extra_attributes(

hooks/openfeature-hooks-opentelemetry/tests/test_metric.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def mock_get_meter(monkeypatch):
1717
"feature_flag.evaluation.error_total": Mock(spec=metrics.Counter),
1818
"feature_flag.evaluation.success_total": Mock(spec=metrics.Counter),
1919
"feature_flag.evaluation.request_total": Mock(spec=metrics.Counter),
20+
"feature_flag.evaluation.duration": Mock(spec=metrics.Histogram),
2021
}
2122

2223
def side_effect(*args, **kwargs):
@@ -26,6 +27,7 @@ def side_effect(*args, **kwargs):
2627
spec=metrics.Meter,
2728
create_up_down_counter=side_effect,
2829
create_counter=side_effect,
30+
create_histogram=side_effect,
2931
)
3032
monkeypatch.setattr(metrics, "get_meter", lambda name: mock_meter)
3133

@@ -211,3 +213,36 @@ def test_metric_finally_after(mock_get_meter):
211213
mock_counters["feature_flag.evaluation.success_total"].add.assert_not_called()
212214
mock_counters["feature_flag.evaluation.request_total"].add.assert_not_called()
213215
mock_counters["feature_flag.evaluation.error_total"].add.assert_not_called()
216+
217+
218+
def test_metric_duration(mock_get_meter):
219+
_, mock_counters = mock_get_meter
220+
hook = MetricsHook()
221+
hook_context = HookContext(
222+
flag_key="flag_key",
223+
flag_type=FlagType.BOOLEAN,
224+
default_value=False,
225+
evaluation_context=EvaluationContext(),
226+
provider_metadata=Metadata(name="test-provider"),
227+
)
228+
details = FlagEvaluationDetails(
229+
flag_key="flag_key",
230+
value=True,
231+
variant="enabled",
232+
reason=Reason.TARGETING_MATCH,
233+
error_code=None,
234+
error_message=None,
235+
)
236+
hook.before(hook_context, hints={})
237+
hook.finally_after(hook_context, details, hints={})
238+
239+
call_args = mock_counters["feature_flag.evaluation.duration"].record.call_args
240+
assert call_args is not None
241+
elapsed, attributes = call_args.args
242+
assert isinstance(elapsed, float)
243+
assert attributes == {
244+
"feature_flag.key": "flag_key",
245+
"feature_flag.provider.name": "test-provider",
246+
}
247+
mock_counters["feature_flag.evaluation.success_total"].add.assert_not_called()
248+
mock_counters["feature_flag.evaluation.error_total"].add.assert_not_called()

0 commit comments

Comments
 (0)