Skip to content

Commit 2fd80ae

Browse files
authored
feat: Add hooks implementation (#95)
Signed-off-by: Maxim Fischuk <[email protected]>
1 parent 40431aa commit 2fd80ae

18 files changed

+1924
-106
lines changed

Diff for: Cargo.toml

+9-4
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@ lazy_static = "1.5"
2121
mockall = { version = "0.13.0", optional = true }
2222
serde_json = { version = "1.0.116", optional = true }
2323
time = "0.3.36"
24-
tokio = { version = "1.40", features = [ "full" ] }
24+
tokio = { version = "1.40", features = ["full"] }
2525
typed-builder = "0.20.0"
2626

27+
log = { package = "log", version = "0.4", optional = true }
28+
2729
[dev-dependencies]
30+
env_logger = "0.11.5"
31+
structured-logger = "1.0.3"
2832
spec = { path = "spec" }
2933

3034
[features]
31-
default = [ "test-util" ]
32-
test-util = [ "dep:mockall" ]
33-
serde_json = [ "dep:serde_json" ]
35+
default = ["test-util", "dep:log"]
36+
test-util = ["dep:mockall"]
37+
serde_json = ["dep:serde_json"]
38+
structured-logging = ["log?/kv"]

Diff for: README.md

+124-18
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,12 @@ See [here](https://docs.rs/open-feature/latest/open_feature/index.html) for the
151151
| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
152152
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
153153
|| [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
154-
| | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
155-
| | [Logging](#logging) | Integrate with popular logging packages. |
154+
| | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
155+
| | [Logging](#logging) | Integrate with popular logging packages. |
156156
|| [Named clients](#named-clients) | Utilize multiple providers in a single application. |
157157
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
158158
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
159-
| | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
159+
| | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
160160

161161
<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>
162162

@@ -211,21 +211,89 @@ client.get_int_value("flag", Some(&evaluation_context), None);
211211

212212
### Hooks
213213

214-
Hooks are not yet available in the Rust SDK.
215-
216-
<!-- TOOD: Uncomment it when we support events
217214
[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle.
218215
Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Rust) for a complete list of available hooks.
219216
If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself.
220217

221218
Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level.
222-
-->
223219

224-
<!-- TODO: code example of setting hooks at all levels -->
220+
```rust
221+
let mut api = OpenFeature::singleton_mut().await;
222+
223+
// Set a global hook.
224+
api.set_hook(MyHook::default()).await;
225+
226+
// Create a client and set a client level hook.
227+
let client = api.create_client();
228+
client.set_hook(MyHook::default());
229+
230+
// Get a flag value with a hook.
231+
let eval = EvaluationOptions::default().with_hook(MyHook::default());
232+
client.get_int_value("key", None, Some(&eval)).await;
233+
```
234+
235+
Example of a hook implementation you can find in [examples/hooks.rs](https://github.com/open-feature/rust-sdk/blob/main/examples/hooks.rs).
236+
237+
To run the example, execute the following command:
238+
239+
```shell
240+
cargo run --example hooks
241+
```
225242

226243
### Logging
227244

228-
Logging customization is not yet available in the Rust SDK.
245+
Note that in accordance with the OpenFeature specification, the SDK doesn't generally log messages during flag evaluation.
246+
247+
#### Logging hook
248+
249+
The Rust SDK provides a logging hook that can be used to log messages during flag evaluation.
250+
This hook is not enabled by default and must be explicitly set.
251+
252+
```rust
253+
let mut api = OpenFeature::singleton_mut().await;
254+
255+
let client = api.create_client().with_logging_hook(false);
256+
257+
...
258+
259+
// Note: You can include evaluation context to log output.
260+
let client = api.create_client().with_logging_hook(true);
261+
```
262+
263+
Both **text** and **structured** logging are supported.
264+
To enable **structured** logging, enable feature `structured-logging` in your `Cargo.toml`:
265+
266+
```toml
267+
open-feature = { version = "0.2.4", features = ["structured-logging"] }
268+
```
269+
270+
Example of a logging hook usage you can find in [examples/logging.rs](https://github.com/open-feature/rust-sdk/blob/main/examples/logging.rs).
271+
272+
To run the example, execute the following command:
273+
274+
```shell
275+
cargo run --example logging
276+
```
277+
278+
**Output**:
279+
280+
```text
281+
[2025-01-10T18:53:11Z DEBUG open_feature::hooks::logging] Before stage: domain=, provider_name=Dummy Provider, flag_key=my_feature, default_value=Some(Bool(false)), evaluation_context=EvaluationContext { targeting_key: None, custom_fields: {} }
282+
[2025-01-10T18:53:11Z DEBUG open_feature::hooks::logging] After stage: domain=, provider_name=Dummy Provider, flag_key=my_feature, default_value=Some(Bool(false)), reason=None, variant=None, value=Bool(true), evaluation_context=EvaluationContext { targeting_key: None, custom_fields: {} }
283+
```
284+
285+
or with structured logging:
286+
287+
```shell
288+
cargo run --example logging --features structured-logging
289+
```
290+
291+
**Output**:
292+
293+
```jsonl
294+
{"default_value":"Some(Bool(false))","domain":"","evaluation_context":"EvaluationContext { targeting_key: None, custom_fields: {} }","flag_key":"my_feature","level":"DEBUG","message":"Before stage","provider_name":"No-op Provider","target":"open_feature","timestamp":1736537120828}
295+
{"default_value":"Some(Bool(false))","domain":"","error_message":"Some(\"No-op provider is never ready\")","evaluation_context":"EvaluationContext { targeting_key: None, custom_fields: {} }","file":"src/hooks/logging.rs","flag_key":"my_feature","level":"ERROR","line":162,"message":"Error stage","module":"open_feature::hooks::logging::structured","provider_name":"No-op Provider","target":"open_feature","timestamp":1736537120828}
296+
```
229297

230298
### Named clients
231299

@@ -281,21 +349,59 @@ Check the source of [`NoOpProvider`](https://github.com/open-feature/rust-sdk/bl
281349
282350
### Develop a hook
283351

284-
Hooks are not yet available in the Rust SDK.
285-
286-
<!-- TOOD: Uncomment it when we support events
287352
To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency.
288353
This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/rust-sdk-contrib) available under the OpenFeature organization.
289354
Implement your own hook by conforming to the `Hook interface`.
290-
To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined.
291-
To avoid defining empty functions make use of the `UnimplementedHook` struct (which already implements all the empty functions).
292-
-->
355+
To satisfy the interface, all methods (`before`/`after`/`finally`/`error`) need to be defined.
293356

294-
<!-- TODO: code example of hook implementation -->
357+
```rust
358+
use open_feature::{
359+
EvaluationContext, EvaluationDetails, EvaluationError,
360+
Hook, HookContext, HookHints, Value,
361+
};
362+
363+
struct MyHook;
364+
365+
#[async_trait::async_trait]
366+
impl Hook for MyHook {
367+
async fn before<'a>(
368+
&self,
369+
context: &HookContext<'a>,
370+
hints: Option<&'a HookHints>,
371+
) -> Result<Option<EvaluationContext>, EvaluationError> {
372+
todo!()
373+
}
374+
375+
async fn after<'a>(
376+
&self,
377+
context: &HookContext<'a>,
378+
details: &EvaluationDetails<Value>,
379+
hints: Option<&'a HookHints>,
380+
) -> Result<(), EvaluationError> {
381+
todo!()
382+
}
383+
384+
async fn error<'a>(
385+
&self,
386+
context: &HookContext<'a>,
387+
error: &EvaluationError,
388+
hints: Option<&'a HookHints>,
389+
) {
390+
todo!()
391+
}
392+
393+
async fn finally<'a>(
394+
&self,
395+
context: &HookContext<'a>,
396+
detaild: &EvaluationDetails<Value>,
397+
hints: Option<&'a HookHints>,
398+
) {
399+
todo!()
400+
}
401+
}
402+
```
295403

296-
<!--
297404
> Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs!
298-
-->
299405
300406
<!-- x-hide-in-docs-start -->
301407
## ⭐️ Support the project

Diff for: examples/hooks.rs

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
use open_feature::{
2+
provider::{FeatureProvider, ProviderMetadata, ProviderStatus, ResolutionDetails},
3+
EvaluationContext, EvaluationDetails, EvaluationError, EvaluationOptions, EvaluationResult,
4+
Hook, HookContext, HookHints, OpenFeature, StructValue, Value,
5+
};
6+
7+
struct DummyProvider(ProviderMetadata);
8+
9+
impl Default for DummyProvider {
10+
fn default() -> Self {
11+
Self(ProviderMetadata::new("Dummy Provider"))
12+
}
13+
}
14+
15+
#[async_trait::async_trait]
16+
impl FeatureProvider for DummyProvider {
17+
fn metadata(&self) -> &ProviderMetadata {
18+
&self.0
19+
}
20+
21+
fn status(&self) -> ProviderStatus {
22+
ProviderStatus::Ready
23+
}
24+
25+
async fn resolve_bool_value(
26+
&self,
27+
_flag_key: &str,
28+
_evaluation_context: &EvaluationContext,
29+
) -> EvaluationResult<ResolutionDetails<bool>> {
30+
Ok(ResolutionDetails::new(true))
31+
}
32+
33+
async fn resolve_int_value(
34+
&self,
35+
_flag_key: &str,
36+
_evaluation_context: &EvaluationContext,
37+
) -> EvaluationResult<ResolutionDetails<i64>> {
38+
unimplemented!()
39+
}
40+
41+
async fn resolve_float_value(
42+
&self,
43+
_flag_key: &str,
44+
_evaluation_context: &EvaluationContext,
45+
) -> EvaluationResult<ResolutionDetails<f64>> {
46+
unimplemented!()
47+
}
48+
49+
async fn resolve_string_value(
50+
&self,
51+
_flag_key: &str,
52+
_evaluation_context: &EvaluationContext,
53+
) -> EvaluationResult<ResolutionDetails<String>> {
54+
unimplemented!()
55+
}
56+
57+
async fn resolve_struct_value(
58+
&self,
59+
_flag_key: &str,
60+
_evaluation_context: &EvaluationContext,
61+
) -> Result<ResolutionDetails<StructValue>, EvaluationError> {
62+
unimplemented!()
63+
}
64+
}
65+
66+
struct DummyLoggingHook(String);
67+
68+
#[async_trait::async_trait]
69+
impl Hook for DummyLoggingHook {
70+
async fn before<'a>(
71+
&self,
72+
context: &HookContext<'a>,
73+
_hints: Option<&'a HookHints>,
74+
) -> Result<Option<EvaluationContext>, EvaluationError> {
75+
log::info!(
76+
"Evaluating({}) flag {} of type {}",
77+
self.0,
78+
context.flag_key,
79+
context.flag_type
80+
);
81+
82+
Ok(None)
83+
}
84+
85+
async fn after<'a>(
86+
&self,
87+
context: &HookContext<'a>,
88+
details: &EvaluationDetails<Value>,
89+
_hints: Option<&'a HookHints>,
90+
) -> Result<(), EvaluationError> {
91+
log::info!(
92+
"Flag({}) {} of type {} evaluated to {:?}",
93+
self.0,
94+
context.flag_key,
95+
context.flag_type,
96+
details.value
97+
);
98+
99+
Ok(())
100+
}
101+
102+
async fn error<'a>(
103+
&self,
104+
context: &HookContext<'a>,
105+
error: &EvaluationError,
106+
_hints: Option<&'a HookHints>,
107+
) {
108+
log::error!(
109+
"Error({}) evaluating flag {} of type {}: {:?}",
110+
self.0,
111+
context.flag_key,
112+
context.flag_type,
113+
error
114+
);
115+
}
116+
117+
async fn finally<'a>(
118+
&self,
119+
context: &HookContext<'a>,
120+
_: &EvaluationDetails<Value>,
121+
_hints: Option<&'a HookHints>,
122+
) {
123+
log::info!(
124+
"Finally({}) evaluating flag {} of type {}",
125+
self.0,
126+
context.flag_key,
127+
context.flag_type
128+
);
129+
}
130+
}
131+
132+
#[tokio::main]
133+
async fn main() {
134+
env_logger::builder()
135+
.filter_level(log::LevelFilter::Info)
136+
.init();
137+
138+
let mut api = OpenFeature::singleton_mut().await;
139+
api.set_provider(DummyProvider::default()).await;
140+
api.add_hook(DummyLoggingHook("global".to_string())).await;
141+
drop(api);
142+
143+
let client = OpenFeature::singleton()
144+
.await
145+
.create_client()
146+
.with_hook(DummyLoggingHook("client".to_string())); // Add a client-level hook
147+
148+
let eval = EvaluationOptions::default().with_hook(DummyLoggingHook("eval".to_string()));
149+
let feature = client
150+
.get_bool_details("my_feature", None, Some(&eval))
151+
.await
152+
.unwrap();
153+
154+
println!("Feature value: {}", feature.value);
155+
}

Diff for: examples/logging.rs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use open_feature::{provider::NoOpProvider, EvaluationOptions, OpenFeature};
2+
3+
#[tokio::main]
4+
async fn main() {
5+
init_logger();
6+
7+
let mut api = OpenFeature::singleton_mut().await;
8+
api.set_provider(NoOpProvider::default()).await;
9+
drop(api);
10+
11+
let client = OpenFeature::singleton()
12+
.await
13+
.create_client()
14+
.with_logging_hook(true); // Add a client-level hook
15+
16+
let eval = EvaluationOptions::default();
17+
let _ = client
18+
.get_bool_details("my_feature", None, Some(&eval))
19+
.await;
20+
}
21+
22+
#[cfg(not(feature = "structured-logging"))]
23+
fn init_logger() {
24+
env_logger::builder()
25+
.filter_level(log::LevelFilter::Debug)
26+
.init();
27+
}
28+
29+
#[cfg(feature = "structured-logging")]
30+
fn init_logger() {
31+
structured_logger::Builder::with_level("debug").init();
32+
}

0 commit comments

Comments
 (0)