Skip to content

Commit

Permalink
feat: add flagd Provider (#19)
Browse files Browse the repository at this point in the history
Signed-off-by: Eren Atas <[email protected]>
Reviewed-by: Alan dos Santos <[email protected]>
Reviewed-by: Kaan Karakaya <[email protected]>
Reviewed-by: Vinicius Gobbo <[email protected]>
  • Loading branch information
erenatas authored Feb 20, 2025
1 parent f084cab commit a1a8573
Show file tree
Hide file tree
Showing 46 changed files with 6,505 additions and 2 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Update git submodules
run: git submodule update --init --recursive

- name: Install protobuf compiler
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler
- name: Setup cache
uses: Swatinem/rust-cache@v2

Expand Down
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "crates/flagd/flagd-testbed"]
path = crates/flagd/flagd-testbed
url = https://github.com/open-feature/flagd-testbed.git
[submodule "crates/flagd/schemas"]
path = crates/flagd/schemas
url = https://github.com/open-feature/flagd-schemas
40 changes: 38 additions & 2 deletions crates/flagd/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,45 @@
[package]
name = "open-feature-flagd"
version = "0.1.0"
version = "0.0.1"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[build-dependencies]
tonic-build = "0.12"
prost = "0.13"
prost-build = "0.13"

[dev-dependencies]
cucumber = "0.21"
tokio-stream = "0.1"
futures-core = "0.3"
testcontainers = { version = "0.23.1", features = ["http_wait", "blocking"] }
wiremock = "0.6.2"
tempfile = "3.3.1"
serial_test = "3.2"
tracing-test = "0.2"
test-log = { version = "0.2", features = ["trace"] }

[dependencies]
open-feature = "0.2"
open-feature = "0.2"
async-trait = "0.1"
tonic = { version = "0.12", features = ["tls"] }
prost = "0.13"
prost-types = "0.13"
tokio = { version = "1.0", features = ["full"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
lru = "0.13"
futures = "0.3"
reqwest = { version = "0.12", features = ["json", "stream"] }
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1.0.95"
regex = "1.11.1"
semver = "1.0.25"
murmurhash3 = "0.0.5"
tower = "0.5"
hyper-util = { version = "0.1", features = ["tokio"] }
thiserror = "2.0"
datalogic-rs = "2.0.16"
186 changes: 186 additions & 0 deletions crates/flagd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
[Generated by cargo-readme: `cargo readme --no-title > README.md`]::
# flagd Provider for OpenFeature

A Rust implementation of the OpenFeature provider for flagd, enabling dynamic
feature flag evaluation in your applications.

This provider supports multiple evaluation modes, advanced targeting rules, caching strategies,
and connection management. It is designed to work seamlessly with the OpenFeature SDK and the flagd service.

### Core Features

- **Multiple Evaluation Modes**
- **RPC Resolver (Remote Evaluation):** Uses gRPC to perform flag evaluations remotely at a flagd instance. Supports bi-directional streaming, retry backoff, and custom name resolution (including Envoy support).
- **REST Resolver:** Uses the OpenFeature Remote Evaluation Protocol (OFREP) over HTTP to evaluate flags.
- **In-Process Resolver:** Performs evaluations locally using an embedded evaluation engine. Flag configurations can be retrieved via gRPC (sync mode).
- **File Resolver:** Operates entirely from a flag definition file, updating on file changes in a best-effort manner.

- **Advanced Targeting**
- **Fractional Rollouts:** Uses consistent hashing (implemented via murmurhash3) to split traffic between flag variants in configurable proportions.
- **Semantic Versioning:** Compare values using common operators such as '=', '!=', '<', '<=', '>', '>=', '^', and '~'.
- **String Operations:** Custom operators for performing “starts_with” and “ends_with” comparisons.
- **Complex Targeting Rules:** Leverages JSONLogic and custom operators to support nested conditions and dynamic evaluation.

- **Caching Strategies**
- Built-in support for LRU caching as well as an in-memory alternative. Flag evaluation results can be cached and later returned with a “CACHED” reason until the configuration updates.

- **Connection Management**
- Automatic connection establishment with configurable retries, timeout settings, and custom TLS or Unix-socket options.
- Support for upstream name resolution including a custom resolver for Envoy proxy integration.

### Installation
Add the dependency in your `Cargo.toml`:
```toml
[dependencies]
open-feature-flagd = "0.0.1"
open-feature = "0.2"
```
Then integrate it into your application:

```rust
use open_feature_flagd::{FlagdOptions, FlagdProvider, ResolverType};
use open_feature::provider::FeatureProvider;
use open_feature::EvaluationContext;

#[tokio::main]
async fn main() {
// Example using the REST resolver mode.
let provider = FlagdProvider::new(FlagdOptions {
host: "localhost".to_string(),
port: 8016,
resolver_type: ResolverType::Rest,
..Default::default()
}).await.unwrap();

let context = EvaluationContext::default().with_targeting_key("user-123");
let result = provider.resolve_bool_value("bool-flag", &context).await.unwrap();
println!("Flag value: {}", result.value);
}
```

### Evaluation Modes
#### Remote Resolver (RPC)
In RPC mode, the provider communicates with flagd via gRPC. It supports features like streaming updates, retry mechanisms, and name resolution (including Envoy).

```rust
use open_feature_flagd::{FlagdOptions, FlagdProvider, ResolverType};
use open_feature::provider::FeatureProvider;
use open_feature::EvaluationContext;

#[tokio::main]
async fn main() {
let provider = FlagdProvider::new(FlagdOptions {
host: "localhost".to_string(),
port: 8013,
resolver_type: ResolverType::Rpc,
..Default::default()
}).await.unwrap();

let context = EvaluationContext::default().with_targeting_key("user-123");
let bool_result = provider.resolve_bool_value("feature-enabled", &context).await.unwrap();
println!("Feature enabled: {}", bool_result.value);
}
```

#### REST Resolver
In REST mode the provider uses the OpenFeature Remote Evaluation Protocol (OFREP) over HTTP.
It is useful when gRPC is not an option.
```rust
use open_feature_flagd::{FlagdOptions, FlagdProvider, ResolverType};
use open_feature::provider::FeatureProvider;
use open_feature::EvaluationContext;

#[tokio::main]
async fn main() {
let provider = FlagdProvider::new(FlagdOptions {
host: "localhost".to_string(),
port: 8016,
resolver_type: ResolverType::Rest,
..Default::default()
}).await.unwrap();

let context = EvaluationContext::default().with_targeting_key("user-456");
let result = provider.resolve_string_value("feature-variant", &context).await.unwrap();
println!("Variant: {}", result.value);
}
```

#### In-Process Resolver
In-process evaluation is performed locally. Flag configurations are sourced via gRPC sync stream.
This mode supports advanced targeting operators (fractional, semver, string comparisons)
using the built-in evaluation engine.
```rust
use open_feature_flagd::{CacheSettings, FlagdOptions, FlagdProvider, ResolverType};
use open_feature::provider::FeatureProvider;
use open_feature::EvaluationContext;

#[tokio::main]
async fn main() {
let provider = FlagdProvider::new(FlagdOptions {
host: "localhost".to_string(),
port: 8015,
resolver_type: ResolverType::InProcess,
selector: Some("my-service".to_string()),
cache_settings: Some(CacheSettings::default()),
..Default::default()
}).await.unwrap();

let context = EvaluationContext::default()
.with_targeting_key("user-abc")
.with_custom_field("environment", "production")
.with_custom_field("semver", "2.1.0");

let dark_mode = provider.resolve_bool_value("dark-mode", &context).await.unwrap();
println!("Dark mode enabled: {}", dark_mode.value);
}
```

#### File Mode
File mode is an in-process variant where flag configurations are read from a file.
This is useful for development or environments without network access.
```rust
use open_feature_flagd::{FlagdOptions, FlagdProvider, ResolverType};
use open_feature::provider::FeatureProvider;
use open_feature::EvaluationContext;

#[tokio::main]
async fn main() {
let file_path = "./path/to/flagd-config.json".to_string();
let provider = FlagdProvider::new(FlagdOptions {
host: "localhost".to_string(),
resolver_type: ResolverType::File,
source_configuration: Some(file_path),
..Default::default()
}).await.unwrap();

let context = EvaluationContext::default();
let result = provider.resolve_int_value("rollout-percentage", &context).await.unwrap();
println!("Rollout percentage: {}", result.value);
}
```

### Configuration Options
Configurations can be provided as constructor options or via environment variables (with constructor options taking priority). The following options are supported:

| Option | Env Variable | Type / Supported Value | Default | Compatible Resolver |
|-----------------------------------------|-----------------------------------------|-----------------------------------|-------------------------------------|--------------------------------|
| Host | FLAGD_HOST | string | "localhost" | RPC, REST, In-Process, File |
| Port | FLAGD_PORT | number | 8013 (RPC), 8016 (REST) | RPC, REST, In-Process, File |
| Target URI | FLAGD_TARGET_URI | string | "" | RPC, In-Process |
| TLS | FLAGD_TLS | boolean | false | RPC, In-Process |
| Socket Path | FLAGD_SOCKET_PATH | string | "" | RPC |
| Certificate Path | FLAGD_SERVER_CERT_PATH | string | "" | RPC, In-Process |
| Cache Type (LRU / In-Memory / Disabled) | FLAGD_CACHE | string ("lru", "mem", "disabled") | lru | RPC, In-Process, File |
| Cache TTL (Seconds) | FLAGD_CACHE_TTL | number | 60 | RPC, In-Process, File |
| Max Cache Size | FLAGD_MAX_CACHE_SIZE | number | 1000 | RPC, In-Process, File |
| Offline File Path | FLAGD_OFFLINE_FLAG_SOURCE_PATH | string | "" | File |
| Retry Backoff (ms) | FLAGD_RETRY_BACKOFF_MS | number | 1000 | RPC, In-Process |
| Retry Backoff Maximum (ms) | FLAGD_RETRY_BACKOFF_MAX_MS | number | 120000 | RPC, In-Process |
| Retry Grace Period | FLAGD_RETRY_GRACE_PERIOD | number | 5 | RPC, In-Process |
| Event Stream Deadline (ms) | FLAGD_STREAM_DEADLINE_MS | number | 600000 | RPC |
| Offline Poll Interval (ms) | FLAGD_OFFLINE_POLL_MS | number | 5000 | File |
| Source Selector | FLAGD_SOURCE_SELECTOR | string | "" | In-Process |

### License
Apache 2.0 - See [LICENSE](./../../LICENSE) for more information.

6 changes: 6 additions & 0 deletions crates/flagd/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Missing Features compared to Golang version:

- Custom connector interface
- Unix socket support for in-process evaluation (CANNOT FIND THIS IN FLAGD IMPLEMENTATION, SOCKET ONLY OPENS FOR GRPC EVALUATION, NOT SYNC)
- Test coverage
- E2E Testing selector
15 changes: 15 additions & 0 deletions crates/flagd/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
fn main() {
let out_dir = std::env::var("OUT_DIR").unwrap();

tonic_build::configure()
.build_server(true)
.out_dir(&out_dir)
.compile_protos(
&[
"schemas/protobuf/flagd/evaluation/v1/evaluation.proto",
"schemas/protobuf/flagd/sync/v1/sync.proto",
],
&["schemas/protobuf/"],
)
.unwrap();
}
28 changes: 28 additions & 0 deletions crates/flagd/docs/contributing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Contributing
## Development
After cloning the repository, you first need to update git odules:
```bash
pushd rust-sdk-contrib/crates/flagd
# Update and pull git submodules
git submodule update --init
```
Afterwards, you need to install `protoc`:
- For MacOS: `brew install protobuf`
- For Fedora: `dnf install protobuf protobuf-devel`

Once steps mentioned above are done, `cargo build` will build crate.

## Testing
To run tests across a flagd server, `testcontainers-rs` crate been used to spin up containers. `Docker` is needed to be alled to run E2E tests.
> At the time of writing, `podman` was tested and did not work.
If it is not possible to access docker, unit tests can be run :
```bash
cargo test --lib
```

open-feature-flagd uses `test-log` to have tracing logs table. To have full visibility on test logs, you can use:

```bash
RUST_LOG_SPAN_EVENTS=full RUST_LOG=debug cargo test -- --nocapture
```
1 change: 1 addition & 0 deletions crates/flagd/flagd-testbed
Submodule flagd-testbed added at 9d35a0
1 change: 1 addition & 0 deletions crates/flagd/schemas
Submodule schemas added at bb7634
Loading

0 comments on commit a1a8573

Please sign in to comment.