Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add hooks implementation #95

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

MaximFischuk
Copy link

@MaximFischuk MaximFischuk commented Dec 16, 2024

This PR

This pull request introduces the implementation of Hooks in the OpenFeature SDK following the OpenFeature standard. The changes include the addition of hooks to various stages in the feature flag evaluation process, such as before, after, error, and finally. The implementation provides capabilities to add hooks at the global, client, provider, and invocation levels, which allows for extensive customizability and control over the feature flag lifecycle.

  • Adds hooks to support the OpenFeature specification for flag evaluation.
  • Implements the capability to register hooks at various levels including global, client, provider, and invocation.
  • Introduces logging for lifecycle events using hooks.
  • Provides test cases for the new hooks capability to ensure compliance with the OpenFeature specification.

Related Issues

Fixes #5

Notes

  • Make sure logging is appropriately configured in the consuming application to take advantage of the logging hooks.
  • This implementation supports extensibility for additional hooks in the future by following the standardized hook interface.

Follow-up Tasks

  • Consider implementing example hooks that demonstrate different use cases, like metrics collection or error reporting.
  • Evaluate if default hooks should be provided in the SDK for common use cases.

How to test

  1. Build and run the example included in examples/hooks.rs to see a working implementation of the hooks in action.
  2. Review test cases provided in src/hooks/mod.rs to ensure they cover expected behaviors and edge cases.
  3. Ensure that the hooks are called in the correct order and can handle errors gracefully.

@MaximFischuk MaximFischuk changed the title Feature/hooks feat: Add hooks implementation Dec 16, 2024
@MaximFischuk MaximFischuk force-pushed the feature/hooks branch 4 times, most recently from 1d86d02 to fafe675 Compare December 17, 2024 09:53
@MaximFischuk MaximFischuk marked this pull request as ready for review December 18, 2024 13:35
) {
log::error!("Error hook for flag {}: {:?}", context.flag_key, error);
}
async fn finally<'a>(&self, context: &HookContext<'a>, _: Option<&'a HookHints>) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async fn finally<'a>(&self, context: &HookContext<'a>, _: Option<&'a HookHints>) {
async fn finally<'a>(&self, context: &HookContext<'a>, _: &EvaluationDetails<Value>, _: Option<&'a HookHints>) {

Hey @MaximFischuk, the finally stage is hooks changed slightly in a recent spec update. Thanks!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the information! Seems like I've missed it, I'll update it soon :)

…ment 4.3.8` of OpenFeature specification

Signed-off-by: MaximFischuk <[email protected]>
@beeme1mr
Copy link
Member

Hey @MaximFischuk, reviews may be slower than usual around the holidays. Hopefully, someone can take a look soon.

I'll do my best to review, but my Rust skills are basically non-existent 😄

Copy link
Member

@gruebel gruebel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall great job 🍻

Cargo.toml Outdated
@@ -2,7 +2,7 @@
name = "open-feature"
version = "0.2.4"
edition = "2021"
rust-version = "1.71.1" # MSRV
rust-version = "1.71.1" # MSRV
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something went wrong here 😅

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, TOML formatter did weird thing :)

src/api/api.rs Outdated
@@ -54,6 +56,12 @@ impl OpenFeature {
self.provider_registry.set_named(name, provider).await;
}

/// Add a new hook to the global list of hooks.
pub async fn add_hook<T: Hook>(&mut self, hook: T) {
let mut lock = self.hooks.get_mut().await; // TODO: Remove async context to avoid deadlock and enforce initialization before usage.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure about the TODO here. didn't you RwLock guard hooks anyway?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of my notes were accidentally published to PR, sorry, I'll remove them

@@ -11,7 +11,7 @@ pub enum EvaluationContextFieldValue {
Float(f64),
String(String),
DateTime(OffsetDateTime),
Struct(Arc<dyn Any + Send + Sync>),
Struct(Arc<dyn Any + Send + Sync>), // TODO: better to make structure in similar way as serde_json::Value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean to create a nested HashMap struct?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly! Type information is completely cleared here, so the client cannot restore the initial type without workarounds. My suggestion is to add a recursion Map like it was implemented in serde_json.

@@ -23,20 +25,26 @@ pub struct Client {
provider_registry: ProviderRegistry,
evaluation_context: EvaluationContext,
global_evaluation_context: GlobalEvaluationContext,
global_hooks: GlobalHooks,

hooks: Vec<HookWrapper>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you rename it to client_hooks?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure :)

.await;
hook_context.evaluation_context = &context;

// INFO: Result of the resolution or error reson with default value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// INFO: Result of the resolution or error reson with default value
// INFO: Result of the resolution or error reason with default value

context: &HookContext<'a>,
_: Option<&'a HookHints>,
) -> Result<Option<EvaluationContext>, EvaluationError> {
log::debug!("Before hook for flag {}", context.flag_key);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just thinking, wouldn't it be better to raise the log levels, debug -> info and the trace -> debug?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's debatable, enterprise clients usually hide debug level of logs in production. Flag evaluating could generate extremely many logs in large applications so it may force a client to disable logs specifically for the OpenFeature library because just setting log level info is not enough.
But, I don't insist :)

@@ -20,7 +20,7 @@ fn json_value_to_value(value: &serde_json::Value) -> EvaluationResult<Value> {
match value {
serde_json::Value::Bool(value) => Ok(Value::Bool(*value)),
serde_json::Value::Number(value) if value.is_i64() => {
Ok(Value::Int(value.as_i64().unwrap()))
Ok(Value::Int(value.as_i64().unwrap())) //TODO: remove unwrap or use expect
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain the TODO? how do you wan to remove the unwrap()?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, it is better to avoid any potential panic in libraries, so in my opinion, the error could be returned to the caller as EvaluationResult::Err(...), or when any error is impossible then expect will make it clearer to anyone who gets an unexpected panic.

use super::{Hook, HookContext, HookHints};

/// A hook that logs the evaluation lifecycle of a flag.
pub struct LoggingHook;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MaximFischuk please consider adding a print option to the LoggingHook struct for evaluation context; it's important to print the context in a readable JSON format if it is set according to the spec

_: &EvaluationDetails<Value>,
_: Option<&'a HookHints>,
) -> Result<(), EvaluationError> {
log::debug!("After hook for flag {}", context.flag_key);
Copy link

@jbovet jbovet Dec 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MaximFischuk please consider adding fields (Logged data) to the log messages (serialized, opt-in) according to the specification in each Stage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[SDK] implement hooks
4 participants