Skip to content

Commit

Permalink
feat: Add hooks implementation (#95)
Browse files Browse the repository at this point in the history
Signed-off-by: Maxim Fischuk <[email protected]>
  • Loading branch information
MaximFischuk authored Jan 13, 2025
1 parent 40431aa commit 2fd80ae
Show file tree
Hide file tree
Showing 18 changed files with 1,924 additions and 106 deletions.
13 changes: 9 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ lazy_static = "1.5"
mockall = { version = "0.13.0", optional = true }
serde_json = { version = "1.0.116", optional = true }
time = "0.3.36"
tokio = { version = "1.40", features = [ "full" ] }
tokio = { version = "1.40", features = ["full"] }
typed-builder = "0.20.0"

log = { package = "log", version = "0.4", optional = true }

[dev-dependencies]
env_logger = "0.11.5"
structured-logger = "1.0.3"
spec = { path = "spec" }

[features]
default = [ "test-util" ]
test-util = [ "dep:mockall" ]
serde_json = [ "dep:serde_json" ]
default = ["test-util", "dep:log"]
test-util = ["dep:mockall"]
serde_json = ["dep:serde_json"]
structured-logging = ["log?/kv"]
142 changes: 124 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,12 @@ See [here](https://docs.rs/open-feature/latest/open_feature/index.html) for the
| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
|| [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
| | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
| | [Logging](#logging) | Integrate with popular logging packages. |
| | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
| | [Logging](#logging) | Integrate with popular logging packages. |
|| [Named clients](#named-clients) | Utilize multiple providers in a single application. |
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
| | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
| | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |

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

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

### Hooks

Hooks are not yet available in the Rust SDK.

<!-- TOOD: Uncomment it when we support events
[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.
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.
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.

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

<!-- TODO: code example of setting hooks at all levels -->
```rust
let mut api = OpenFeature::singleton_mut().await;

// Set a global hook.
api.set_hook(MyHook::default()).await;

// Create a client and set a client level hook.
let client = api.create_client();
client.set_hook(MyHook::default());

// Get a flag value with a hook.
let eval = EvaluationOptions::default().with_hook(MyHook::default());
client.get_int_value("key", None, Some(&eval)).await;
```

Example of a hook implementation you can find in [examples/hooks.rs](https://github.com/open-feature/rust-sdk/blob/main/examples/hooks.rs).

To run the example, execute the following command:

```shell
cargo run --example hooks
```

### Logging

Logging customization is not yet available in the Rust SDK.
Note that in accordance with the OpenFeature specification, the SDK doesn't generally log messages during flag evaluation.

#### Logging hook

The Rust SDK provides a logging hook that can be used to log messages during flag evaluation.
This hook is not enabled by default and must be explicitly set.

```rust
let mut api = OpenFeature::singleton_mut().await;

let client = api.create_client().with_logging_hook(false);

...

// Note: You can include evaluation context to log output.
let client = api.create_client().with_logging_hook(true);
```

Both **text** and **structured** logging are supported.
To enable **structured** logging, enable feature `structured-logging` in your `Cargo.toml`:

```toml
open-feature = { version = "0.2.4", features = ["structured-logging"] }
```

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).

To run the example, execute the following command:

```shell
cargo run --example logging
```

**Output**:

```text
[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: {} }
[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: {} }
```

or with structured logging:

```shell
cargo run --example logging --features structured-logging
```

**Output**:

```jsonl
{"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}
{"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}
```

### Named clients

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

Hooks are not yet available in the Rust SDK.

<!-- TOOD: Uncomment it when we support events
To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency.
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.
Implement your own hook by conforming to the `Hook interface`.
To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined.
To avoid defining empty functions make use of the `UnimplementedHook` struct (which already implements all the empty functions).
-->
To satisfy the interface, all methods (`before`/`after`/`finally`/`error`) need to be defined.

<!-- TODO: code example of hook implementation -->
```rust
use open_feature::{
EvaluationContext, EvaluationDetails, EvaluationError,
Hook, HookContext, HookHints, Value,
};

struct MyHook;

#[async_trait::async_trait]
impl Hook for MyHook {
async fn before<'a>(
&self,
context: &HookContext<'a>,
hints: Option<&'a HookHints>,
) -> Result<Option<EvaluationContext>, EvaluationError> {
todo!()
}

async fn after<'a>(
&self,
context: &HookContext<'a>,
details: &EvaluationDetails<Value>,
hints: Option<&'a HookHints>,
) -> Result<(), EvaluationError> {
todo!()
}

async fn error<'a>(
&self,
context: &HookContext<'a>,
error: &EvaluationError,
hints: Option<&'a HookHints>,
) {
todo!()
}

async fn finally<'a>(
&self,
context: &HookContext<'a>,
detaild: &EvaluationDetails<Value>,
hints: Option<&'a HookHints>,
) {
todo!()
}
}
```

<!--
> 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!
-->
<!-- x-hide-in-docs-start -->
## ⭐️ Support the project
Expand Down
155 changes: 155 additions & 0 deletions examples/hooks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use open_feature::{
provider::{FeatureProvider, ProviderMetadata, ProviderStatus, ResolutionDetails},
EvaluationContext, EvaluationDetails, EvaluationError, EvaluationOptions, EvaluationResult,
Hook, HookContext, HookHints, OpenFeature, StructValue, Value,
};

struct DummyProvider(ProviderMetadata);

impl Default for DummyProvider {
fn default() -> Self {
Self(ProviderMetadata::new("Dummy Provider"))
}
}

#[async_trait::async_trait]
impl FeatureProvider for DummyProvider {
fn metadata(&self) -> &ProviderMetadata {
&self.0
}

fn status(&self) -> ProviderStatus {
ProviderStatus::Ready
}

async fn resolve_bool_value(
&self,
_flag_key: &str,
_evaluation_context: &EvaluationContext,
) -> EvaluationResult<ResolutionDetails<bool>> {
Ok(ResolutionDetails::new(true))
}

async fn resolve_int_value(
&self,
_flag_key: &str,
_evaluation_context: &EvaluationContext,
) -> EvaluationResult<ResolutionDetails<i64>> {
unimplemented!()
}

async fn resolve_float_value(
&self,
_flag_key: &str,
_evaluation_context: &EvaluationContext,
) -> EvaluationResult<ResolutionDetails<f64>> {
unimplemented!()
}

async fn resolve_string_value(
&self,
_flag_key: &str,
_evaluation_context: &EvaluationContext,
) -> EvaluationResult<ResolutionDetails<String>> {
unimplemented!()
}

async fn resolve_struct_value(
&self,
_flag_key: &str,
_evaluation_context: &EvaluationContext,
) -> Result<ResolutionDetails<StructValue>, EvaluationError> {
unimplemented!()
}
}

struct DummyLoggingHook(String);

#[async_trait::async_trait]
impl Hook for DummyLoggingHook {
async fn before<'a>(
&self,
context: &HookContext<'a>,
_hints: Option<&'a HookHints>,
) -> Result<Option<EvaluationContext>, EvaluationError> {
log::info!(
"Evaluating({}) flag {} of type {}",
self.0,
context.flag_key,
context.flag_type
);

Ok(None)
}

async fn after<'a>(
&self,
context: &HookContext<'a>,
details: &EvaluationDetails<Value>,
_hints: Option<&'a HookHints>,
) -> Result<(), EvaluationError> {
log::info!(
"Flag({}) {} of type {} evaluated to {:?}",
self.0,
context.flag_key,
context.flag_type,
details.value
);

Ok(())
}

async fn error<'a>(
&self,
context: &HookContext<'a>,
error: &EvaluationError,
_hints: Option<&'a HookHints>,
) {
log::error!(
"Error({}) evaluating flag {} of type {}: {:?}",
self.0,
context.flag_key,
context.flag_type,
error
);
}

async fn finally<'a>(
&self,
context: &HookContext<'a>,
_: &EvaluationDetails<Value>,
_hints: Option<&'a HookHints>,
) {
log::info!(
"Finally({}) evaluating flag {} of type {}",
self.0,
context.flag_key,
context.flag_type
);
}
}

#[tokio::main]
async fn main() {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();

let mut api = OpenFeature::singleton_mut().await;
api.set_provider(DummyProvider::default()).await;
api.add_hook(DummyLoggingHook("global".to_string())).await;
drop(api);

let client = OpenFeature::singleton()
.await
.create_client()
.with_hook(DummyLoggingHook("client".to_string())); // Add a client-level hook

let eval = EvaluationOptions::default().with_hook(DummyLoggingHook("eval".to_string()));
let feature = client
.get_bool_details("my_feature", None, Some(&eval))
.await
.unwrap();

println!("Feature value: {}", feature.value);
}
32 changes: 32 additions & 0 deletions examples/logging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use open_feature::{provider::NoOpProvider, EvaluationOptions, OpenFeature};

#[tokio::main]
async fn main() {
init_logger();

let mut api = OpenFeature::singleton_mut().await;
api.set_provider(NoOpProvider::default()).await;
drop(api);

let client = OpenFeature::singleton()
.await
.create_client()
.with_logging_hook(true); // Add a client-level hook

let eval = EvaluationOptions::default();
let _ = client
.get_bool_details("my_feature", None, Some(&eval))
.await;
}

#[cfg(not(feature = "structured-logging"))]
fn init_logger() {
env_logger::builder()
.filter_level(log::LevelFilter::Debug)
.init();
}

#[cfg(feature = "structured-logging")]
fn init_logger() {
structured_logger::Builder::with_level("debug").init();
}
Loading

0 comments on commit 2fd80ae

Please sign in to comment.