From 9297a8772c9bfe28d1b6ab95daef9549910bca35 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sat, 25 Apr 2026 11:38:38 -0400 Subject: [PATCH 01/26] feat(shipping): delay international shipments based on shippingSlowdown feature flag Closes #346 Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/flagd/demo.flagd.json | 9 ++++ src/shipping/src/shipping_service.rs | 19 ++++++- .../src/shipping_service/feature_flag.rs | 51 +++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/shipping/src/shipping_service/feature_flag.rs diff --git a/src/flagd/demo.flagd.json b/src/flagd/demo.flagd.json index 44864130c9..b78efa00a9 100644 --- a/src/flagd/demo.flagd.json +++ b/src/flagd/demo.flagd.json @@ -146,6 +146,15 @@ "10000x": 10000 }, "defaultVariant": "off" + }, + "shippingSlowdown": { + "description": "Delays international shipping responses to simulate overseas shipping delay", + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "off" } } } diff --git a/src/shipping/src/shipping_service.rs b/src/shipping/src/shipping_service.rs index 5dd3bc75d3..d6e791a59a 100644 --- a/src/shipping/src/shipping_service.rs +++ b/src/shipping/src/shipping_service.rs @@ -4,6 +4,8 @@ use actix_web::{post, web, HttpResponse, Responder}; use tracing::info; +mod feature_flag; + mod quote; use quote::create_quote_from_count; @@ -14,6 +16,7 @@ mod shipping_types; pub use shipping_types::*; const NANOS_MULTIPLE: u32 = 10000000u32; +const SLOWDOWN_SECS: u64 = 10; #[post("/get-quote")] pub async fn get_quote(req: web::Json) -> impl Responder { @@ -45,7 +48,21 @@ pub async fn get_quote(req: web::Json) -> impl Responder { } #[post("/ship-order")] -pub async fn ship_order(_req: web::Json) -> impl Responder { +pub async fn ship_order(req: web::Json) -> impl Responder { + let is_outside_us = req + .address + .as_ref() + .map(|addr| addr.country.to_uppercase() != "US") + .unwrap_or(false); + + if is_outside_us && feature_flag::is_feature_flag_enabled("shippingSlowdown").await { + info!( + name = "ShippingSlowdown", + message = "Delaying international shipment due to shippingSlowdown feature flag" + ); + actix_web::rt::time::sleep(std::time::Duration::from_secs(SLOWDOWN_SECS)).await; + } + let tid = create_tracking_id(); info!( name = "CreatingTrackingId", diff --git a/src/shipping/src/shipping_service/feature_flag.rs b/src/shipping/src/shipping_service/feature_flag.rs new file mode 100644 index 0000000000..bc33334a4b --- /dev/null +++ b/src/shipping/src/shipping_service/feature_flag.rs @@ -0,0 +1,51 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +use awc::Client; +use serde::Deserialize; +use std::env; +use tracing::warn; + +#[derive(Debug, Deserialize)] +struct OFREPResponse { + value: bool, +} + +/// Checks whether a boolean feature flag is enabled via the flagd OFREP REST API. +/// Returns `false` on any error so the service degrades gracefully. +pub async fn is_feature_flag_enabled(flag_name: &str) -> bool { + let host = env::var("FLAGD_HOST").unwrap_or_else(|_| "flagd".to_string()); + let port = env::var("FLAGD_OFREP_PORT") + .ok() + .and_then(|p| p.parse::().ok()) + .unwrap_or(8016); + + let url = format!( + "http://{}:{}/ofrep/v1/evaluate/flags/{}", + host, port, flag_name + ); + + let client = Client::default(); + let result = client + .post(&url) + .insert_header(("Content-Type", "application/json")) + .send_body(r#"{"context":{}}"#) + .await; + + match result { + Ok(mut resp) => match resp.json::().await { + Ok(data) => data.value, + Err(e) => { + warn!( + "Failed to parse feature flag response for {}: {}", + flag_name, e + ); + false + } + }, + Err(e) => { + warn!("Failed to check feature flag {}: {}", flag_name, e); + false + } + } +} From 2cad4302ea3151bdee958ed9d4bac1db1911a064 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sat, 25 Apr 2026 11:42:54 -0400 Subject: [PATCH 02/26] feat(shipping): add OTel tracing to feature flag evaluation Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- .../src/shipping_service/feature_flag.rs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/shipping/src/shipping_service/feature_flag.rs b/src/shipping/src/shipping_service/feature_flag.rs index bc33334a4b..6b57b666be 100644 --- a/src/shipping/src/shipping_service/feature_flag.rs +++ b/src/shipping/src/shipping_service/feature_flag.rs @@ -2,6 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use awc::Client; +use opentelemetry::{ + global, + trace::{Span, SpanKind, Tracer}, + KeyValue, +}; +use opentelemetry_instrumentation_actix_web::ClientExt; use serde::Deserialize; use std::env; use tracing::warn; @@ -9,11 +15,21 @@ use tracing::warn; #[derive(Debug, Deserialize)] struct OFREPResponse { value: bool, + variant: Option, } /// Checks whether a boolean feature flag is enabled via the flagd OFREP REST API. /// Returns `false` on any error so the service degrades gracefully. pub async fn is_feature_flag_enabled(flag_name: &str) -> bool { + let tracer = global::tracer("otel_demo.shipping.feature_flag"); + let mut span = tracer + .span_builder(format!("feature_flag {}", flag_name)) + .with_kind(SpanKind::Client) + .start(&tracer); + + span.set_attribute(KeyValue::new("feature_flag.key", flag_name.to_string())); + span.set_attribute(KeyValue::new("feature_flag.provider_name", "flagd")); + let host = env::var("FLAGD_HOST").unwrap_or_else(|_| "flagd".to_string()); let port = env::var("FLAGD_OFREP_PORT") .ok() @@ -29,12 +45,21 @@ pub async fn is_feature_flag_enabled(flag_name: &str) -> bool { let result = client .post(&url) .insert_header(("Content-Type", "application/json")) + .trace_request() .send_body(r#"{"context":{}}"#) .await; match result { Ok(mut resp) => match resp.json::().await { - Ok(data) => data.value, + Ok(data) => { + if let Some(variant) = &data.variant { + span.set_attribute(KeyValue::new( + "feature_flag.variant", + variant.clone(), + )); + } + data.value + } Err(e) => { warn!( "Failed to parse feature flag response for {}: {}", From d6669698537f84bed845f45f15ee5272f15dba2b Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sun, 26 Apr 2026 21:27:32 -0400 Subject: [PATCH 03/26] test(shipping): add ship_order tests with injectable flag checker and slowdown duration Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/main.rs | 4 +- src/shipping/src/shipping_service.rs | 98 ++++++++++++++++++++++++---- 2 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/shipping/src/main.rs b/src/shipping/src/main.rs index 102bcbd9dd..91aa4eefa5 100644 --- a/src/shipping/src/main.rs +++ b/src/shipping/src/main.rs @@ -9,7 +9,7 @@ use tracing::info; mod telemetry_conf; use telemetry_conf::init_otel; mod shipping_service; -use shipping_service::{get_quote, ship_order}; +use shipping_service::{default_flag_checker, get_quote, ship_order, DEFAULT_SLOWDOWN}; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -45,6 +45,8 @@ async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() + .app_data(actix_web::web::Data::new(default_flag_checker())) + .app_data(actix_web::web::Data::new(DEFAULT_SLOWDOWN)) .wrap(RequestTracing::new()) .wrap(RequestMetrics::default()) .service(get_quote) diff --git a/src/shipping/src/shipping_service.rs b/src/shipping/src/shipping_service.rs index d6e791a59a..9359aa19d4 100644 --- a/src/shipping/src/shipping_service.rs +++ b/src/shipping/src/shipping_service.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use actix_web::{post, web, HttpResponse, Responder}; +use std::{future::Future, pin::Pin, sync::Arc}; use tracing::info; mod feature_flag; @@ -15,8 +16,17 @@ use tracking::create_tracking_id; mod shipping_types; pub use shipping_types::*; +pub type FlagChecker = + Arc Pin>> + Send + Sync>; + +pub fn default_flag_checker() -> FlagChecker { + Arc::new(|flag: String| { + Box::pin(async move { feature_flag::is_feature_flag_enabled(&flag).await }) + }) +} + const NANOS_MULTIPLE: u32 = 10000000u32; -const SLOWDOWN_SECS: u64 = 10; +pub const DEFAULT_SLOWDOWN: std::time::Duration = std::time::Duration::from_secs(10); #[post("/get-quote")] pub async fn get_quote(req: web::Json) -> impl Responder { @@ -48,19 +58,23 @@ pub async fn get_quote(req: web::Json) -> impl Responder { } #[post("/ship-order")] -pub async fn ship_order(req: web::Json) -> impl Responder { +pub async fn ship_order( + req: web::Json, + flag_checker: web::Data, + slowdown: web::Data, +) -> impl Responder { let is_outside_us = req .address .as_ref() .map(|addr| addr.country.to_uppercase() != "US") .unwrap_or(false); - if is_outside_us && feature_flag::is_feature_flag_enabled("shippingSlowdown").await { + if is_outside_us && flag_checker("shippingSlowdown".to_string()).await { info!( name = "ShippingSlowdown", message = "Delaying international shipment due to shippingSlowdown feature flag" ); - actix_web::rt::time::sleep(std::time::Duration::from_secs(SLOWDOWN_SECS)).await; + actix_web::rt::time::sleep(**slowdown).await; } let tid = create_tracking_id(); @@ -78,21 +92,81 @@ mod tests { use super::*; - #[actix_web::test] - async fn test_ship_order() { - let app = test::init_service(App::new().service(ship_order)).await; + fn mock_checker(enabled: bool) -> web::Data { + let checker: FlagChecker = if enabled { + Arc::new(|_| Box::pin(async { true })) + } else { + Arc::new(|_| Box::pin(async { false })) + }; + web::Data::new(checker) + } + + async fn call_ship_order( + address: Option
, + flag_enabled: bool, + slowdown: std::time::Duration, + ) -> ShipOrderResponse { + let app = test::init_service( + App::new() + .app_data(mock_checker(flag_enabled)) + .app_data(web::Data::new(slowdown)) + .service(ship_order), + ) + .await; let req = test::TestRequest::post() .uri("/ship-order") .insert_header(ContentType::json()) - .set_json(&ShipOrderRequest { - address: None, - items: vec![], - }) + .set_json(&ShipOrderRequest { address, items: vec![] }) .to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); + test::read_body_json(resp).await + } + + fn make_address(country: &str) -> Address { + Address { + street_address: "123 Main St".into(), + city: "Anytown".into(), + state: "CA".into(), + country: country.into(), + zip_code: "00000".into(), + } + } + + #[actix_web::test] + async fn test_ship_order_no_address() { + let order = call_ship_order(None, false, std::time::Duration::ZERO).await; + assert!(!order.tracking_id.is_empty()); + } - let order: ShipOrderResponse = test::read_body_json(resp).await; + #[actix_web::test] + async fn test_ship_order_us_address() { + let order = call_ship_order(Some(make_address("US")), false, std::time::Duration::ZERO).await; + assert!(!order.tracking_id.is_empty()); + } + + #[actix_web::test] + async fn test_ship_order_international_flag_off() { + let order = call_ship_order(Some(make_address("CA")), false, std::time::Duration::ZERO).await; + assert!(!order.tracking_id.is_empty()); + } + + #[actix_web::test] + async fn test_ship_order_international_flag_on() { + let slowdown = std::time::Duration::from_millis(50); + let start = std::time::Instant::now(); + let order = call_ship_order(Some(make_address("CA")), true, slowdown).await; + assert!(start.elapsed() >= slowdown); + assert!(!order.tracking_id.is_empty()); + } + + #[actix_web::test] + async fn test_ship_order_us_flag_on() { + // US addresses must not be delayed even when the flag is on + let slowdown = std::time::Duration::from_millis(50); + let start = std::time::Instant::now(); + let order = call_ship_order(Some(make_address("US")), true, slowdown).await; + assert!(start.elapsed() < slowdown); assert!(!order.tracking_id.is_empty()); } } From 30aeeacd17dc6e4184196cce194832dd211dfab8 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sun, 26 Apr 2026 21:38:26 -0400 Subject: [PATCH 04/26] test(shipping): add ship_order tests with injectable flag checker and slowdown duration Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/shipping_service.rs | 44 +++++++++++++++++++--------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/shipping/src/shipping_service.rs b/src/shipping/src/shipping_service.rs index 9359aa19d4..7006f3e83a 100644 --- a/src/shipping/src/shipping_service.rs +++ b/src/shipping/src/shipping_service.rs @@ -92,23 +92,37 @@ mod tests { use super::*; - fn mock_checker(enabled: bool) -> web::Data { - let checker: FlagChecker = if enabled { - Arc::new(|_| Box::pin(async { true })) - } else { - Arc::new(|_| Box::pin(async { false })) - }; - web::Data::new(checker) + struct MockFlagChecker { + flags: std::collections::HashMap<&'static str, bool>, + } + + impl MockFlagChecker { + fn new() -> Self { + Self { flags: std::collections::HashMap::new() } + } + + fn with_flag(mut self, flag: &'static str, enabled: bool) -> Self { + self.flags.insert(flag, enabled); + self + } + + fn build(self) -> web::Data { + let map = self.flags; + web::Data::new(Arc::new(move |flag: String| { + let value = map.get(flag.as_str()).copied().unwrap_or(false); + Box::pin(async move { value }) as Pin>> + }) as FlagChecker) + } } async fn call_ship_order( address: Option
, - flag_enabled: bool, + checker: web::Data, slowdown: std::time::Duration, ) -> ShipOrderResponse { let app = test::init_service( App::new() - .app_data(mock_checker(flag_enabled)) + .app_data(checker) .app_data(web::Data::new(slowdown)) .service(ship_order), ) @@ -135,19 +149,19 @@ mod tests { #[actix_web::test] async fn test_ship_order_no_address() { - let order = call_ship_order(None, false, std::time::Duration::ZERO).await; + let order = call_ship_order(None, MockFlagChecker::new().build(), std::time::Duration::ZERO).await; assert!(!order.tracking_id.is_empty()); } #[actix_web::test] async fn test_ship_order_us_address() { - let order = call_ship_order(Some(make_address("US")), false, std::time::Duration::ZERO).await; + let order = call_ship_order(Some(make_address("US")), MockFlagChecker::new().build(), std::time::Duration::ZERO).await; assert!(!order.tracking_id.is_empty()); } #[actix_web::test] async fn test_ship_order_international_flag_off() { - let order = call_ship_order(Some(make_address("CA")), false, std::time::Duration::ZERO).await; + let order = call_ship_order(Some(make_address("CA")), MockFlagChecker::new().build(), std::time::Duration::ZERO).await; assert!(!order.tracking_id.is_empty()); } @@ -155,7 +169,8 @@ mod tests { async fn test_ship_order_international_flag_on() { let slowdown = std::time::Duration::from_millis(50); let start = std::time::Instant::now(); - let order = call_ship_order(Some(make_address("CA")), true, slowdown).await; + let checker = MockFlagChecker::new().with_flag("shippingSlowdown", true).build(); + let order = call_ship_order(Some(make_address("CA")), checker, slowdown).await; assert!(start.elapsed() >= slowdown); assert!(!order.tracking_id.is_empty()); } @@ -165,7 +180,8 @@ mod tests { // US addresses must not be delayed even when the flag is on let slowdown = std::time::Duration::from_millis(50); let start = std::time::Instant::now(); - let order = call_ship_order(Some(make_address("US")), true, slowdown).await; + let checker = MockFlagChecker::new().with_flag("shippingSlowdown", true).build(); + let order = call_ship_order(Some(make_address("US")), checker, slowdown).await; assert!(start.elapsed() < slowdown); assert!(!order.tracking_id.is_empty()); } From b5ff292f98097790064dd14a00d120b0f3c3a46a Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sun, 26 Apr 2026 23:12:35 -0400 Subject: [PATCH 05/26] feat(shipping): add FLAGD_HOST, FLAGD_OFREP_PORT env vars and flagd dependency to compose Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- docker-compose.minimal.yml | 4 ++++ docker-compose.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docker-compose.minimal.yml b/docker-compose.minimal.yml index f2291fd6ca..7a90b709e6 100644 --- a/docker-compose.minimal.yml +++ b/docker-compose.minimal.yml @@ -562,6 +562,8 @@ services: - IPV6_ENABLED - SHIPPING_PORT - QUOTE_ADDR + - FLAGD_HOST + - FLAGD_OFREP_PORT - OTEL_EXPORTER_OTLP_ENDPOINT - OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES},service.criticality=high - OTEL_SERVICE_NAME=shipping @@ -575,6 +577,8 @@ services: depends_on: otel-collector: condition: service_started + flagd: + condition: service_started logging: *logging # ****************** diff --git a/docker-compose.yml b/docker-compose.yml index 6e69ce3135..c0cea7c545 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -649,6 +649,8 @@ services: - IPV6_ENABLED - SHIPPING_PORT - QUOTE_ADDR + - FLAGD_HOST + - FLAGD_OFREP_PORT - OTEL_EXPORTER_OTLP_ENDPOINT - OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES},service.criticality=high - OTEL_SERVICE_NAME=shipping @@ -656,6 +658,8 @@ services: depends_on: otel-collector: condition: service_started + flagd: + condition: service_started logging: *logging # ****************** From 6e3b78a3ca077525557c726796268b2ef3fe8c1a Mon Sep 17 00:00:00 2001 From: tlee439 Date: Thu, 7 May 2026 12:20:07 -0400 Subject: [PATCH 06/26] docs: add shippingSlowdown feature flag entry to CHANGELOG Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92ba6644ed..785b2c308c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ the release. ## Unreleased +* [shipping] Add `shippingSlowdown` feature flag to delay international (non-US) + shipments via flagd OFREP integration with OpenTelemetry tracing + ([#346](https://github.com/open-telemetry/opentelemetry-demo/issues/346)) * [collector] Add `transform/sanitize_logs` processor to work around `otelcol.signal` scope attribute conflict with `otelcol.signal.output` that causes OpenSearch/Elasticsearch mapping failures From cabb716a41cd358250bb6e8b950e993697538382 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sat, 9 May 2026 14:19:51 -0400 Subject: [PATCH 07/26] fix(shipping): check HTTP status before parsing feature flag response Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/shipping_service/feature_flag.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/shipping/src/shipping_service/feature_flag.rs b/src/shipping/src/shipping_service/feature_flag.rs index 6b57b666be..a058353aba 100644 --- a/src/shipping/src/shipping_service/feature_flag.rs +++ b/src/shipping/src/shipping_service/feature_flag.rs @@ -50,7 +50,7 @@ pub async fn is_feature_flag_enabled(flag_name: &str) -> bool { .await; match result { - Ok(mut resp) => match resp.json::().await { + Ok(mut resp) if resp.status().is_success() => match resp.json::().await { Ok(data) => { if let Some(variant) = &data.variant { span.set_attribute(KeyValue::new( @@ -68,6 +68,12 @@ pub async fn is_feature_flag_enabled(flag_name: &str) -> bool { false } }, + Ok(resp) => { + warn!( + "Feature flag {} returned HTTP {}", flag_name, resp.status() + ); + false + } Err(e) => { warn!("Failed to check feature flag {}: {}", flag_name, e); false From 5476d2aad375451391814324ce03243ce4081170 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sat, 9 May 2026 14:23:31 -0400 Subject: [PATCH 08/26] docs(shipping): document shippingSlowdown feature flag and env vars Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/shipping/README.md b/src/shipping/README.md index 915a78aba3..61847b646e 100644 --- a/src/shipping/README.md +++ b/src/shipping/README.md @@ -21,3 +21,16 @@ docker compose build shipping ```sh cargo test ``` + +## Feature Flags + +* `shippingSlowdown`: when enabled, non-US shipping requests are delayed by 10 + seconds to simulate overseas shipping latency. US addresses are never affected. + The flag is evaluated via the flagd OFREP REST API. + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `FLAGD_HOST` | `flagd` | Hostname of the flagd service | +| `FLAGD_OFREP_PORT` | `8016` | Port for the flagd OFREP API | From 2cab5e247ee8b866bad7b788caeeddea18f5d754 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sat, 9 May 2026 14:49:30 -0400 Subject: [PATCH 09/26] fix(shipping): match all common US country name variants Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/shipping_service.rs | 30 +++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/shipping/src/shipping_service.rs b/src/shipping/src/shipping_service.rs index 7006f3e83a..1259cd0ab3 100644 --- a/src/shipping/src/shipping_service.rs +++ b/src/shipping/src/shipping_service.rs @@ -66,7 +66,12 @@ pub async fn ship_order( let is_outside_us = req .address .as_ref() - .map(|addr| addr.country.to_uppercase() != "US") + .map(|addr| { + !matches!( + addr.country.to_uppercase().trim(), + "US" | "USA" | "UNITED STATES" | "UNITED STATES OF AMERICA" + ) + }) .unwrap_or(false); if is_outside_us && flag_checker("shippingSlowdown".to_string()).await { @@ -177,12 +182,31 @@ mod tests { #[actix_web::test] async fn test_ship_order_us_flag_on() { - // US addresses must not be delayed even when the flag is on let slowdown = std::time::Duration::from_millis(50); - let start = std::time::Instant::now(); let checker = MockFlagChecker::new().with_flag("shippingSlowdown", true).build(); + let start = std::time::Instant::now(); let order = call_ship_order(Some(make_address("US")), checker, slowdown).await; assert!(start.elapsed() < slowdown); assert!(!order.tracking_id.is_empty()); } + + #[actix_web::test] + async fn test_ship_order_united_states_flag_on() { + let slowdown = std::time::Duration::from_millis(50); + let checker = MockFlagChecker::new().with_flag("shippingSlowdown", true).build(); + let start = std::time::Instant::now(); + let order = call_ship_order(Some(make_address("United States")), checker, slowdown).await; + assert!(start.elapsed() < slowdown); + assert!(!order.tracking_id.is_empty()); + } + + #[actix_web::test] + async fn test_ship_order_usa_flag_on() { + let slowdown = std::time::Duration::from_millis(50); + let checker = MockFlagChecker::new().with_flag("shippingSlowdown", true).build(); + let start = std::time::Instant::now(); + let order = call_ship_order(Some(make_address("USA")), checker, slowdown).await; + assert!(start.elapsed() < slowdown); + assert!(!order.tracking_id.is_empty()); + } } From cae3f18cca620a3ea536ba5e8acf451a6d803b5c Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sat, 9 May 2026 15:09:09 -0400 Subject: [PATCH 10/26] test(shipping): remove redundant US country variant tests Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/shipping_service.rs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/shipping/src/shipping_service.rs b/src/shipping/src/shipping_service.rs index 1259cd0ab3..74a0404904 100644 --- a/src/shipping/src/shipping_service.rs +++ b/src/shipping/src/shipping_service.rs @@ -189,24 +189,4 @@ mod tests { assert!(start.elapsed() < slowdown); assert!(!order.tracking_id.is_empty()); } - - #[actix_web::test] - async fn test_ship_order_united_states_flag_on() { - let slowdown = std::time::Duration::from_millis(50); - let checker = MockFlagChecker::new().with_flag("shippingSlowdown", true).build(); - let start = std::time::Instant::now(); - let order = call_ship_order(Some(make_address("United States")), checker, slowdown).await; - assert!(start.elapsed() < slowdown); - assert!(!order.tracking_id.is_empty()); - } - - #[actix_web::test] - async fn test_ship_order_usa_flag_on() { - let slowdown = std::time::Duration::from_millis(50); - let checker = MockFlagChecker::new().with_flag("shippingSlowdown", true).build(); - let start = std::time::Instant::now(); - let order = call_ship_order(Some(make_address("USA")), checker, slowdown).await; - assert!(start.elapsed() < slowdown); - assert!(!order.tracking_id.is_empty()); - } } From 31084146a44316a63632f33a71a7033cb942b7fb Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sat, 9 May 2026 15:17:28 -0400 Subject: [PATCH 11/26] fix(shipping): use tracing crate for feature flag spans instead of raw OTel API Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- .../src/shipping_service/feature_flag.rs | 38 +++++-------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/src/shipping/src/shipping_service/feature_flag.rs b/src/shipping/src/shipping_service/feature_flag.rs index a058353aba..2a0bb2f653 100644 --- a/src/shipping/src/shipping_service/feature_flag.rs +++ b/src/shipping/src/shipping_service/feature_flag.rs @@ -2,15 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use awc::Client; -use opentelemetry::{ - global, - trace::{Span, SpanKind, Tracer}, - KeyValue, -}; use opentelemetry_instrumentation_actix_web::ClientExt; use serde::Deserialize; use std::env; -use tracing::warn; +use tracing::{instrument, warn, Span}; #[derive(Debug, Deserialize)] struct OFREPResponse { @@ -18,18 +13,13 @@ struct OFREPResponse { variant: Option, } -/// Checks whether a boolean feature flag is enabled via the flagd OFREP REST API. -/// Returns `false` on any error so the service degrades gracefully. +#[instrument(fields( + otel.kind = "client", + feature_flag.key = flag_name, + feature_flag.provider_name = "flagd", + feature_flag.variant = tracing::field::Empty, +))] pub async fn is_feature_flag_enabled(flag_name: &str) -> bool { - let tracer = global::tracer("otel_demo.shipping.feature_flag"); - let mut span = tracer - .span_builder(format!("feature_flag {}", flag_name)) - .with_kind(SpanKind::Client) - .start(&tracer); - - span.set_attribute(KeyValue::new("feature_flag.key", flag_name.to_string())); - span.set_attribute(KeyValue::new("feature_flag.provider_name", "flagd")); - let host = env::var("FLAGD_HOST").unwrap_or_else(|_| "flagd".to_string()); let port = env::var("FLAGD_OFREP_PORT") .ok() @@ -53,25 +43,17 @@ pub async fn is_feature_flag_enabled(flag_name: &str) -> bool { Ok(mut resp) if resp.status().is_success() => match resp.json::().await { Ok(data) => { if let Some(variant) = &data.variant { - span.set_attribute(KeyValue::new( - "feature_flag.variant", - variant.clone(), - )); + Span::current().record("feature_flag.variant", variant.as_str()); } data.value } Err(e) => { - warn!( - "Failed to parse feature flag response for {}: {}", - flag_name, e - ); + warn!("Failed to parse feature flag response for {}: {}", flag_name, e); false } }, Ok(resp) => { - warn!( - "Feature flag {} returned HTTP {}", flag_name, resp.status() - ); + warn!("Feature flag {} returned HTTP {}", flag_name, resp.status()); false } Err(e) => { From 28192f5cdadf9aa27fd6f351ea7fff070a214410 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sat, 9 May 2026 15:24:19 -0400 Subject: [PATCH 12/26] fix(shipping): reuse HTTP client for feature flag requests via LazyLock Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/shipping_service/feature_flag.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/shipping/src/shipping_service/feature_flag.rs b/src/shipping/src/shipping_service/feature_flag.rs index 2a0bb2f653..6fdaca3d06 100644 --- a/src/shipping/src/shipping_service/feature_flag.rs +++ b/src/shipping/src/shipping_service/feature_flag.rs @@ -5,8 +5,11 @@ use awc::Client; use opentelemetry_instrumentation_actix_web::ClientExt; use serde::Deserialize; use std::env; +use std::sync::LazyLock; use tracing::{instrument, warn, Span}; +static HTTP_CLIENT: LazyLock = LazyLock::new(Client::default); + #[derive(Debug, Deserialize)] struct OFREPResponse { value: bool, @@ -31,8 +34,7 @@ pub async fn is_feature_flag_enabled(flag_name: &str) -> bool { host, port, flag_name ); - let client = Client::default(); - let result = client + let result = HTTP_CLIENT .post(&url) .insert_header(("Content-Type", "application/json")) .trace_request() From 361df7bf6d9376e8b883802bce6476b50c3926d9 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sat, 9 May 2026 15:51:59 -0400 Subject: [PATCH 13/26] refactor(shipping): restructure feature flag as FlagdClient struct Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- .../src/shipping_service/feature_flag.rs | 93 +++++++++++-------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/src/shipping/src/shipping_service/feature_flag.rs b/src/shipping/src/shipping_service/feature_flag.rs index 6fdaca3d06..6e0bb2307e 100644 --- a/src/shipping/src/shipping_service/feature_flag.rs +++ b/src/shipping/src/shipping_service/feature_flag.rs @@ -8,59 +8,74 @@ use std::env; use std::sync::LazyLock; use tracing::{instrument, warn, Span}; -static HTTP_CLIENT: LazyLock = LazyLock::new(Client::default); - #[derive(Debug, Deserialize)] struct OFREPResponse { value: bool, variant: Option, } -#[instrument(fields( - otel.kind = "client", - feature_flag.key = flag_name, - feature_flag.provider_name = "flagd", - feature_flag.variant = tracing::field::Empty, -))] -pub async fn is_feature_flag_enabled(flag_name: &str) -> bool { - let host = env::var("FLAGD_HOST").unwrap_or_else(|_| "flagd".to_string()); - let port = env::var("FLAGD_OFREP_PORT") - .ok() - .and_then(|p| p.parse::().ok()) - .unwrap_or(8016); +struct FlagdClient { + base_url: String, +} + +impl FlagdClient { + fn from_env() -> Self { + let host = env::var("FLAGD_HOST").unwrap_or_else(|_| "flagd".to_string()); + let port = env::var("FLAGD_OFREP_PORT") + .ok() + .and_then(|p| p.parse::().ok()) + .unwrap_or(8016); + + Self { + base_url: format!("http://{}:{}/ofrep/v1/evaluate/flags", host, port), + } + } - let url = format!( - "http://{}:{}/ofrep/v1/evaluate/flags/{}", - host, port, flag_name - ); + #[instrument(skip(self), fields( + otel.kind = "client", + feature_flag.key = flag_name, + feature_flag.provider_name = "flagd", + feature_flag.variant = tracing::field::Empty, + ))] + async fn is_enabled(&self, flag_name: &str) -> bool { + let url = format!("{}/{}", self.base_url, flag_name); - let result = HTTP_CLIENT - .post(&url) - .insert_header(("Content-Type", "application/json")) - .trace_request() - .send_body(r#"{"context":{}}"#) - .await; + let result = Client::default() + .post(&url) + .insert_header(("Content-Type", "application/json")) + .trace_request() + .send_body(r#"{"context":{}}"#) + .await; - match result { - Ok(mut resp) if resp.status().is_success() => match resp.json::().await { - Ok(data) => { - if let Some(variant) = &data.variant { - Span::current().record("feature_flag.variant", variant.as_str()); + match result { + Ok(mut resp) if resp.status().is_success() => { + match resp.json::().await { + Ok(data) => { + if let Some(variant) = &data.variant { + Span::current().record("feature_flag.variant", variant.as_str()); + } + data.value + } + Err(e) => { + warn!("Failed to parse feature flag response for {}: {}", flag_name, e); + false + } } - data.value + } + Ok(resp) => { + warn!("Feature flag {} returned HTTP {}", flag_name, resp.status()); + false } Err(e) => { - warn!("Failed to parse feature flag response for {}: {}", flag_name, e); + warn!("Failed to check feature flag {}: {}", flag_name, e); false } - }, - Ok(resp) => { - warn!("Feature flag {} returned HTTP {}", flag_name, resp.status()); - false - } - Err(e) => { - warn!("Failed to check feature flag {}: {}", flag_name, e); - false } } } + +static FLAGD: LazyLock = LazyLock::new(FlagdClient::from_env); + +pub async fn is_feature_flag_enabled(flag_name: &str) -> bool { + FLAGD.is_enabled(flag_name).await +} From 9afc6498f907f2f5eeb7836f3e183996cb6d5eee Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sat, 9 May 2026 16:17:13 -0400 Subject: [PATCH 14/26] refactor(shipping): restructure feature flag as FlagdClient struct Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/main.rs | 4 ++-- src/shipping/src/shipping_service.rs | 20 +++++++++++++++---- .../src/shipping_service/feature_flag.rs | 13 ++++-------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/shipping/src/main.rs b/src/shipping/src/main.rs index 91aa4eefa5..91c66ce478 100644 --- a/src/shipping/src/main.rs +++ b/src/shipping/src/main.rs @@ -9,7 +9,7 @@ use tracing::info; mod telemetry_conf; use telemetry_conf::init_otel; mod shipping_service; -use shipping_service::{default_flag_checker, get_quote, ship_order, DEFAULT_SLOWDOWN}; +use shipping_service::{DefaultFlagChecker, get_quote, ship_order, DEFAULT_SLOWDOWN}; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -45,7 +45,7 @@ async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() - .app_data(actix_web::web::Data::new(default_flag_checker())) + .app_data(actix_web::web::Data::new(DefaultFlagChecker::new().build())) .app_data(actix_web::web::Data::new(DEFAULT_SLOWDOWN)) .wrap(RequestTracing::new()) .wrap(RequestMetrics::default()) diff --git a/src/shipping/src/shipping_service.rs b/src/shipping/src/shipping_service.rs index 74a0404904..a6ec8d42a7 100644 --- a/src/shipping/src/shipping_service.rs +++ b/src/shipping/src/shipping_service.rs @@ -6,6 +6,7 @@ use std::{future::Future, pin::Pin, sync::Arc}; use tracing::info; mod feature_flag; +pub use feature_flag::FlagdClient; mod quote; use quote::create_quote_from_count; @@ -19,10 +20,21 @@ pub use shipping_types::*; pub type FlagChecker = Arc Pin>> + Send + Sync>; -pub fn default_flag_checker() -> FlagChecker { - Arc::new(|flag: String| { - Box::pin(async move { feature_flag::is_feature_flag_enabled(&flag).await }) - }) +pub struct DefaultFlagChecker { + client: FlagdClient, +} + +impl DefaultFlagChecker { + pub fn new() -> Self { + Self { client: FlagdClient::from_env() } + } + + pub fn build(self) -> FlagChecker { + Arc::new(move |flag: String| { + let client = self.client.clone(); + Box::pin(async move { client.is_enabled(&flag).await }) + }) + } } const NANOS_MULTIPLE: u32 = 10000000u32; diff --git a/src/shipping/src/shipping_service/feature_flag.rs b/src/shipping/src/shipping_service/feature_flag.rs index 6e0bb2307e..c2d5da4195 100644 --- a/src/shipping/src/shipping_service/feature_flag.rs +++ b/src/shipping/src/shipping_service/feature_flag.rs @@ -5,7 +5,6 @@ use awc::Client; use opentelemetry_instrumentation_actix_web::ClientExt; use serde::Deserialize; use std::env; -use std::sync::LazyLock; use tracing::{instrument, warn, Span}; #[derive(Debug, Deserialize)] @@ -14,12 +13,13 @@ struct OFREPResponse { variant: Option, } -struct FlagdClient { +#[derive(Clone)] +pub struct FlagdClient { base_url: String, } impl FlagdClient { - fn from_env() -> Self { + pub fn from_env() -> Self { let host = env::var("FLAGD_HOST").unwrap_or_else(|_| "flagd".to_string()); let port = env::var("FLAGD_OFREP_PORT") .ok() @@ -37,7 +37,7 @@ impl FlagdClient { feature_flag.provider_name = "flagd", feature_flag.variant = tracing::field::Empty, ))] - async fn is_enabled(&self, flag_name: &str) -> bool { + pub async fn is_enabled(&self, flag_name: &str) -> bool { let url = format!("{}/{}", self.base_url, flag_name); let result = Client::default() @@ -74,8 +74,3 @@ impl FlagdClient { } } -static FLAGD: LazyLock = LazyLock::new(FlagdClient::from_env); - -pub async fn is_feature_flag_enabled(flag_name: &str) -> bool { - FLAGD.is_enabled(flag_name).await -} From 024b8a27bcc7c1bceeb4f581b75e4ecece9f4b65 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Sat, 9 May 2026 19:43:51 -0400 Subject: [PATCH 15/26] fix(shipping): reuse HTTP client per worker thread via thread_local! awc::Client is !Send + !Sync (uses Rc internally), so it cannot be stored in a struct captured by the Send + Sync FlagChecker closure. Instead, store one Client per actix worker thread using thread_local!, initialized lazily on first use. Cloning the Client is cheap (bumps an Rc, not the pool). Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- .../src/shipping_service/feature_flag.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/shipping/src/shipping_service/feature_flag.rs b/src/shipping/src/shipping_service/feature_flag.rs index c2d5da4195..da5d6a5a19 100644 --- a/src/shipping/src/shipping_service/feature_flag.rs +++ b/src/shipping/src/shipping_service/feature_flag.rs @@ -4,9 +4,24 @@ use awc::Client; use opentelemetry_instrumentation_actix_web::ClientExt; use serde::Deserialize; +use std::cell::RefCell; use std::env; use tracing::{instrument, warn, Span}; +thread_local! { + static HTTP_CLIENT: RefCell> = const { RefCell::new(None) }; +} + +fn get_client() -> Client { + HTTP_CLIENT.with(|cell| { + let mut opt = cell.borrow_mut(); + if opt.is_none() { + *opt = Some(Client::default()); + } + opt.as_ref().unwrap().clone() + }) +} + #[derive(Debug, Deserialize)] struct OFREPResponse { value: bool, @@ -40,7 +55,7 @@ impl FlagdClient { pub async fn is_enabled(&self, flag_name: &str) -> bool { let url = format!("{}/{}", self.base_url, flag_name); - let result = Client::default() + let result = get_client() .post(&url) .insert_header(("Content-Type", "application/json")) .trace_request() @@ -73,4 +88,3 @@ impl FlagdClient { } } } - From 672c4d5aafb0642c3fdf905b1add479af6814ed6 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Mon, 11 May 2026 22:58:01 -0400 Subject: [PATCH 16/26] fix(shipping): remove tracing span from feature flag client The #[instrument] span was never bridged to OTel since tracing-opentelemetry is not wired up, so the span and its fields were silently dropped. Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/shipping_service/feature_flag.rs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/shipping/src/shipping_service/feature_flag.rs b/src/shipping/src/shipping_service/feature_flag.rs index da5d6a5a19..29bd823b25 100644 --- a/src/shipping/src/shipping_service/feature_flag.rs +++ b/src/shipping/src/shipping_service/feature_flag.rs @@ -6,7 +6,7 @@ use opentelemetry_instrumentation_actix_web::ClientExt; use serde::Deserialize; use std::cell::RefCell; use std::env; -use tracing::{instrument, warn, Span}; +use tracing::warn; thread_local! { static HTTP_CLIENT: RefCell> = const { RefCell::new(None) }; @@ -46,12 +46,6 @@ impl FlagdClient { } } - #[instrument(skip(self), fields( - otel.kind = "client", - feature_flag.key = flag_name, - feature_flag.provider_name = "flagd", - feature_flag.variant = tracing::field::Empty, - ))] pub async fn is_enabled(&self, flag_name: &str) -> bool { let url = format!("{}/{}", self.base_url, flag_name); @@ -65,12 +59,7 @@ impl FlagdClient { match result { Ok(mut resp) if resp.status().is_success() => { match resp.json::().await { - Ok(data) => { - if let Some(variant) = &data.variant { - Span::current().record("feature_flag.variant", variant.as_str()); - } - data.value - } + Ok(data) => data.value, Err(e) => { warn!("Failed to parse feature flag response for {}: {}", flag_name, e); false From 82945d79ddcacc34f25e3743910fbc6d46dc4121 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Mon, 11 May 2026 23:14:01 -0400 Subject: [PATCH 17/26] feat(shipping): log feature flag evaluation result Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/shipping_service/feature_flag.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/shipping/src/shipping_service/feature_flag.rs b/src/shipping/src/shipping_service/feature_flag.rs index 29bd823b25..753d000b8c 100644 --- a/src/shipping/src/shipping_service/feature_flag.rs +++ b/src/shipping/src/shipping_service/feature_flag.rs @@ -6,7 +6,7 @@ use opentelemetry_instrumentation_actix_web::ClientExt; use serde::Deserialize; use std::cell::RefCell; use std::env; -use tracing::warn; +use tracing::{info, warn}; thread_local! { static HTTP_CLIENT: RefCell> = const { RefCell::new(None) }; @@ -59,7 +59,15 @@ impl FlagdClient { match result { Ok(mut resp) if resp.status().is_success() => { match resp.json::().await { - Ok(data) => data.value, + Ok(data) => { + info!( + feature_flag.key = flag_name, + feature_flag.provider_name = "flagd", + feature_flag.variant = data.variant.as_deref().unwrap_or("unknown"), + "feature flag evaluated" + ); + data.value + } Err(e) => { warn!("Failed to parse feature flag response for {}: {}", flag_name, e); false From 2d5aad357192a6ce1febbb359285257901c12d94 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Tue, 12 May 2026 15:02:15 -0400 Subject: [PATCH 18/26] refactor(shipping): make FlagChecker generic over flag value type Replace closure-based FlagChecker type alias with an enum that supports generic evaluate::() calls, enabling non-bool flags in the future. Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/main.rs | 4 +- src/shipping/src/shipping_service.rs | 78 +++++++++---------- .../src/shipping_service/feature_flag.rs | 16 ++-- 3 files changed, 48 insertions(+), 50 deletions(-) diff --git a/src/shipping/src/main.rs b/src/shipping/src/main.rs index 91c66ce478..2766870cf2 100644 --- a/src/shipping/src/main.rs +++ b/src/shipping/src/main.rs @@ -9,7 +9,7 @@ use tracing::info; mod telemetry_conf; use telemetry_conf::init_otel; mod shipping_service; -use shipping_service::{DefaultFlagChecker, get_quote, ship_order, DEFAULT_SLOWDOWN}; +use shipping_service::{FlagChecker, get_quote, ship_order, DEFAULT_SLOWDOWN}; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -45,7 +45,7 @@ async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() - .app_data(actix_web::web::Data::new(DefaultFlagChecker::new().build())) + .app_data(actix_web::web::Data::new(FlagChecker::from_env())) .app_data(actix_web::web::Data::new(DEFAULT_SLOWDOWN)) .wrap(RequestTracing::new()) .wrap(RequestMetrics::default()) diff --git a/src/shipping/src/shipping_service.rs b/src/shipping/src/shipping_service.rs index a6ec8d42a7..44fa7eb819 100644 --- a/src/shipping/src/shipping_service.rs +++ b/src/shipping/src/shipping_service.rs @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use actix_web::{post, web, HttpResponse, Responder}; -use std::{future::Future, pin::Pin, sync::Arc}; +use serde::de::DeserializeOwned; +use std::any::Any; use tracing::info; mod feature_flag; -pub use feature_flag::FlagdClient; +use feature_flag::FlagdClient; mod quote; use quote::create_quote_from_count; @@ -17,23 +18,30 @@ use tracking::create_tracking_id; mod shipping_types; pub use shipping_types::*; -pub type FlagChecker = - Arc Pin>> + Send + Sync>; - -pub struct DefaultFlagChecker { - client: FlagdClient, +pub enum FlagChecker { + Real(FlagdClient), + #[cfg(test)] + Mock(std::collections::HashMap>), } -impl DefaultFlagChecker { - pub fn new() -> Self { - Self { client: FlagdClient::from_env() } +impl FlagChecker { + pub fn from_env() -> Self { + Self::Real(FlagdClient::from_env()) } - pub fn build(self) -> FlagChecker { - Arc::new(move |flag: String| { - let client = self.client.clone(); - Box::pin(async move { client.is_enabled(&flag).await }) - }) + pub async fn evaluate( + &self, + flag_name: &str, + ) -> T { + match self { + Self::Real(client) => client.evaluate(flag_name).await, + #[cfg(test)] + Self::Mock(flags) => flags + .get(flag_name) + .and_then(|v| v.downcast_ref::()) + .cloned() + .unwrap_or_default(), + } } } @@ -86,7 +94,7 @@ pub async fn ship_order( }) .unwrap_or(false); - if is_outside_us && flag_checker("shippingSlowdown".to_string()).await { + if is_outside_us && flag_checker.evaluate::("shippingSlowdown").await { info!( name = "ShippingSlowdown", message = "Delaying international shipment due to shippingSlowdown feature flag" @@ -109,34 +117,22 @@ mod tests { use super::*; - struct MockFlagChecker { - flags: std::collections::HashMap<&'static str, bool>, + fn mock_checker() -> FlagChecker { + FlagChecker::Mock(std::collections::HashMap::new()) } - impl MockFlagChecker { - fn new() -> Self { - Self { flags: std::collections::HashMap::new() } - } - - fn with_flag(mut self, flag: &'static str, enabled: bool) -> Self { - self.flags.insert(flag, enabled); - self - } - - fn build(self) -> web::Data { - let map = self.flags; - web::Data::new(Arc::new(move |flag: String| { - let value = map.get(flag.as_str()).copied().unwrap_or(false); - Box::pin(async move { value }) as Pin>> - }) as FlagChecker) - } + fn mock_checker_with(flag: &str, value: impl Any + Send + Sync + 'static) -> FlagChecker { + let mut map = std::collections::HashMap::new(); + map.insert(flag.to_string(), Box::new(value) as Box); + FlagChecker::Mock(map) } async fn call_ship_order( address: Option
, - checker: web::Data, + checker: FlagChecker, slowdown: std::time::Duration, ) -> ShipOrderResponse { + let checker = web::Data::new(checker); let app = test::init_service( App::new() .app_data(checker) @@ -166,19 +162,19 @@ mod tests { #[actix_web::test] async fn test_ship_order_no_address() { - let order = call_ship_order(None, MockFlagChecker::new().build(), std::time::Duration::ZERO).await; + let order = call_ship_order(None, mock_checker(), std::time::Duration::ZERO).await; assert!(!order.tracking_id.is_empty()); } #[actix_web::test] async fn test_ship_order_us_address() { - let order = call_ship_order(Some(make_address("US")), MockFlagChecker::new().build(), std::time::Duration::ZERO).await; + let order = call_ship_order(Some(make_address("US")), mock_checker(), std::time::Duration::ZERO).await; assert!(!order.tracking_id.is_empty()); } #[actix_web::test] async fn test_ship_order_international_flag_off() { - let order = call_ship_order(Some(make_address("CA")), MockFlagChecker::new().build(), std::time::Duration::ZERO).await; + let order = call_ship_order(Some(make_address("CA")), mock_checker(), std::time::Duration::ZERO).await; assert!(!order.tracking_id.is_empty()); } @@ -186,7 +182,7 @@ mod tests { async fn test_ship_order_international_flag_on() { let slowdown = std::time::Duration::from_millis(50); let start = std::time::Instant::now(); - let checker = MockFlagChecker::new().with_flag("shippingSlowdown", true).build(); + let checker = mock_checker_with("shippingSlowdown", true); let order = call_ship_order(Some(make_address("CA")), checker, slowdown).await; assert!(start.elapsed() >= slowdown); assert!(!order.tracking_id.is_empty()); @@ -195,7 +191,7 @@ mod tests { #[actix_web::test] async fn test_ship_order_us_flag_on() { let slowdown = std::time::Duration::from_millis(50); - let checker = MockFlagChecker::new().with_flag("shippingSlowdown", true).build(); + let checker = mock_checker_with("shippingSlowdown", true); let start = std::time::Instant::now(); let order = call_ship_order(Some(make_address("US")), checker, slowdown).await; assert!(start.elapsed() < slowdown); diff --git a/src/shipping/src/shipping_service/feature_flag.rs b/src/shipping/src/shipping_service/feature_flag.rs index 753d000b8c..c91bfe1bed 100644 --- a/src/shipping/src/shipping_service/feature_flag.rs +++ b/src/shipping/src/shipping_service/feature_flag.rs @@ -3,6 +3,7 @@ use awc::Client; use opentelemetry_instrumentation_actix_web::ClientExt; +use serde::de::DeserializeOwned; use serde::Deserialize; use std::cell::RefCell; use std::env; @@ -23,8 +24,8 @@ fn get_client() -> Client { } #[derive(Debug, Deserialize)] -struct OFREPResponse { - value: bool, +struct OFREPResponse { + value: T, variant: Option, } @@ -46,7 +47,7 @@ impl FlagdClient { } } - pub async fn is_enabled(&self, flag_name: &str) -> bool { + pub async fn evaluate(&self, flag_name: &str) -> T { let url = format!("{}/{}", self.base_url, flag_name); let result = get_client() @@ -58,7 +59,7 @@ impl FlagdClient { match result { Ok(mut resp) if resp.status().is_success() => { - match resp.json::().await { + match resp.json::>().await { Ok(data) => { info!( feature_flag.key = flag_name, @@ -70,18 +71,19 @@ impl FlagdClient { } Err(e) => { warn!("Failed to parse feature flag response for {}: {}", flag_name, e); - false + T::default() } } } Ok(resp) => { warn!("Feature flag {} returned HTTP {}", flag_name, resp.status()); - false + T::default() } Err(e) => { warn!("Failed to check feature flag {}: {}", flag_name, e); - false + T::default() } } } + } From ac5de4671eb8997aa84bf9e900b31ff241f35e30 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Tue, 12 May 2026 15:30:41 -0400 Subject: [PATCH 19/26] refactor(shipping): move mock constructors into FlagChecker as builder methods Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/shipping_service.rs | 33 +++++++++++++++------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/shipping/src/shipping_service.rs b/src/shipping/src/shipping_service.rs index 44fa7eb819..9b0ff1b150 100644 --- a/src/shipping/src/shipping_service.rs +++ b/src/shipping/src/shipping_service.rs @@ -29,6 +29,19 @@ impl FlagChecker { Self::Real(FlagdClient::from_env()) } + #[cfg(test)] + fn mock() -> Self { + Self::Mock(std::collections::HashMap::new()) + } + + #[cfg(test)] + fn with(mut self, flag: &str, value: impl Any + Send + Sync + 'static) -> Self { + if let Self::Mock(ref mut map) = self { + map.insert(flag.to_string(), Box::new(value) as Box); + } + self + } + pub async fn evaluate( &self, flag_name: &str, @@ -117,16 +130,6 @@ mod tests { use super::*; - fn mock_checker() -> FlagChecker { - FlagChecker::Mock(std::collections::HashMap::new()) - } - - fn mock_checker_with(flag: &str, value: impl Any + Send + Sync + 'static) -> FlagChecker { - let mut map = std::collections::HashMap::new(); - map.insert(flag.to_string(), Box::new(value) as Box); - FlagChecker::Mock(map) - } - async fn call_ship_order( address: Option
, checker: FlagChecker, @@ -162,19 +165,19 @@ mod tests { #[actix_web::test] async fn test_ship_order_no_address() { - let order = call_ship_order(None, mock_checker(), std::time::Duration::ZERO).await; + let order = call_ship_order(None, FlagChecker::mock(), std::time::Duration::ZERO).await; assert!(!order.tracking_id.is_empty()); } #[actix_web::test] async fn test_ship_order_us_address() { - let order = call_ship_order(Some(make_address("US")), mock_checker(), std::time::Duration::ZERO).await; + let order = call_ship_order(Some(make_address("US")), FlagChecker::mock(), std::time::Duration::ZERO).await; assert!(!order.tracking_id.is_empty()); } #[actix_web::test] async fn test_ship_order_international_flag_off() { - let order = call_ship_order(Some(make_address("CA")), mock_checker(), std::time::Duration::ZERO).await; + let order = call_ship_order(Some(make_address("CA")), FlagChecker::mock(), std::time::Duration::ZERO).await; assert!(!order.tracking_id.is_empty()); } @@ -182,7 +185,7 @@ mod tests { async fn test_ship_order_international_flag_on() { let slowdown = std::time::Duration::from_millis(50); let start = std::time::Instant::now(); - let checker = mock_checker_with("shippingSlowdown", true); + let checker = FlagChecker::mock().with("shippingSlowdown", true); let order = call_ship_order(Some(make_address("CA")), checker, slowdown).await; assert!(start.elapsed() >= slowdown); assert!(!order.tracking_id.is_empty()); @@ -191,7 +194,7 @@ mod tests { #[actix_web::test] async fn test_ship_order_us_flag_on() { let slowdown = std::time::Duration::from_millis(50); - let checker = mock_checker_with("shippingSlowdown", true); + let checker = FlagChecker::mock().with("shippingSlowdown", true); let start = std::time::Instant::now(); let order = call_ship_order(Some(make_address("US")), checker, slowdown).await; assert!(start.elapsed() < slowdown); From 76fa0755914ed9537b3583f212b654f519b9f3e1 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Tue, 12 May 2026 17:01:39 -0400 Subject: [PATCH 20/26] fix linting --- src/shipping/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shipping/README.md b/src/shipping/README.md index 61847b646e..de473156e6 100644 --- a/src/shipping/README.md +++ b/src/shipping/README.md @@ -30,7 +30,7 @@ cargo test ## Environment Variables -| Variable | Default | Description | -|----------|---------|-------------| -| `FLAGD_HOST` | `flagd` | Hostname of the flagd service | -| `FLAGD_OFREP_PORT` | `8016` | Port for the flagd OFREP API | +| Variable | Default | Description | +|--------------------|---------|-------------------------------| +| `FLAGD_HOST` | `flagd` | Hostname of the flagd service | +| `FLAGD_OFREP_PORT` | `8016` | Port for the flagd OFREP API | From 2e3539dd4b1da88192c42ebe33e9fd7fe49f3ca3 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Thu, 14 May 2026 21:52:26 -0400 Subject: [PATCH 21/26] refactor(shipping): replace custom OFREP client with open-feature-flagd crate Use FlagdProvider from open-feature-flagd instead of a hand-rolled HTTP client, removing the FlagdClient wrapper and FlagChecker enum. The ship_order handler now takes dyn FeatureProvider directly, and tests use MockFeatureProvider from the open-feature crate. Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/Cargo.lock | 808 +++++++++++++++++- src/shipping/Cargo.toml | 2 + src/shipping/src/main.rs | 30 +- src/shipping/src/shipping_service.rs | 97 +-- .../src/shipping_service/feature_flag.rs | 89 -- 5 files changed, 869 insertions(+), 157 deletions(-) delete mode 100644 src/shipping/src/shipping_service/feature_flag.rs diff --git a/src/shipping/Cargo.lock b/src/shipping/Cargo.lock index 5884cc1dda..1f418ca5b5 100644 --- a/src/shipping/Cargo.lock +++ b/src/shipping/Cargo.lock @@ -37,7 +37,7 @@ dependencies = [ "derive_more", "encoding_rs", "flate2", - "foldhash", + "foldhash 0.1.5", "futures-core", "h2 0.3.27", "http 0.2.12", @@ -170,7 +170,7 @@ dependencies = [ "cookie", "derive_more", "encoding_rs", - "foldhash", + "foldhash 0.1.5", "futures-core", "futures-util", "impl-more", @@ -234,6 +234,27 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "anyhow" version = "1.0.102" @@ -257,6 +278,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "awc" version = "3.8.2" @@ -413,6 +440,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.10.0" @@ -424,6 +457,17 @@ dependencies = [ "rand_core 0.10.0", ] +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + [[package]] name = "const-oid" version = "0.10.2" @@ -450,6 +494,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.3.0" @@ -477,6 +527,18 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "datalogic-rs" +version = "4.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcab7239cbd842e6592ff150b568b4274700de080ff3c3060d80281f47d7c27a" +dependencies = [ + "chrono", + "regex", + "serde", + "serde_json", +] + [[package]] name = "deranged" version = "0.5.8" @@ -531,6 +593,12 @@ dependencies = [ "syn", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "either" version = "1.15.0" @@ -559,15 +627,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -590,6 +670,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -599,6 +685,24 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -671,6 +775,19 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -678,9 +795,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -741,7 +860,18 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] @@ -844,6 +974,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.3.1", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -874,12 +1020,36 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.1", "tokio", "tower-service", "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1007,6 +1177,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inotify" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1058,6 +1248,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1082,6 +1292,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -1120,6 +1336,21 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -1169,6 +1400,71 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "murmurhash3" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2983372caf4480544083767bf2d27defafe32af49ab4df3a0b7fc90793a3664" + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1184,12 +1480,65 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "open-feature" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1ef07989b00b697d9e597e66509e2b0e7a1c889d8925fffe5408703215d72b" +dependencies = [ + "async-trait", + "lazy_static", + "log", + "mockall", + "time", + "tokio", + "typed-builder", +] + +[[package]] +name = "open-feature-flagd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6539a78daae309c8f18dd57f6e20dfdbb5c227dd5737425549c54c303688a065" +dependencies = [ + "async-trait", + "datalogic-rs", + "hyper-util", + "lru", + "murmurhash3", + "notify", + "open-feature", + "prost", + "prost-types", + "reqwest", + "semver", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tower", + "tracing", +] + [[package]] name = "opentelemetry" version = "0.31.0" @@ -1337,6 +1686,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + [[package]] name = "pin-project" version = "1.1.11" @@ -1393,6 +1753,32 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -1422,6 +1808,27 @@ dependencies = [ "prost-derive", ] +[[package]] +name = "prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn", + "tempfile", +] + [[package]] name = "prost-derive" version = "0.14.3" @@ -1435,6 +1842,90 @@ dependencies = [ "syn", ] +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" +dependencies = [ + "pulldown-cmark", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.1", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -1555,25 +2046,53 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1583,6 +2102,54 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1595,6 +2162,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1689,6 +2265,8 @@ dependencies = [ "actix-web", "anyhow", "awc", + "open-feature", + "open-feature-flagd", "opentelemetry", "opentelemetry-appender-tracing", "opentelemetry-instrumentation-actix-web", @@ -1762,6 +2340,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -1793,6 +2377,25 @@ dependencies = [ "syn", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "2.0.18" @@ -1863,6 +2466,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.50.0" @@ -1891,6 +2509,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -1944,6 +2572,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tonic-prost" version = "0.14.5" @@ -1955,6 +2595,22 @@ dependencies = [ "tonic", ] +[[package]] +name = "tonic-prost-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", + "tempfile", + "tonic-build", +] + [[package]] name = "tower" version = "0.5.3" @@ -2072,12 +2728,38 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-builder" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -2096,6 +2778,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -2138,6 +2826,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2251,6 +2949,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -2273,12 +2984,93 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2599,6 +3391,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/src/shipping/Cargo.toml b/src/shipping/Cargo.toml index 06eed57c51..369f6a183c 100644 --- a/src/shipping/Cargo.toml +++ b/src/shipping/Cargo.toml @@ -13,6 +13,8 @@ path = "src/main.rs" actix-web = "4" anyhow = "1.0.102" awc = { version = "3.8.2", default-features = false, features = ["compress-zstd"] } +open-feature = "0.2" +open-feature-flagd = "0.1" serde = { version = "1.0.228", features = ["derive"] } tonic = "0.14.5" tracing = "0.1.44" diff --git a/src/shipping/src/main.rs b/src/shipping/src/main.rs index 2766870cf2..0105a8b543 100644 --- a/src/shipping/src/main.rs +++ b/src/shipping/src/main.rs @@ -1,15 +1,18 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -use actix_web::{App, HttpServer}; +use actix_web::{web, App, HttpServer}; +use open_feature::provider::FeatureProvider; +use open_feature_flagd::{FlagdOptions, FlagdProvider, ResolverType}; use opentelemetry_instrumentation_actix_web::{RequestMetrics, RequestTracing}; use std::env; +use std::sync::Arc; use tracing::info; mod telemetry_conf; use telemetry_conf::init_otel; mod shipping_service; -use shipping_service::{FlagChecker, get_quote, ship_order, DEFAULT_SLOWDOWN}; +use shipping_service::{get_quote, ship_order, DEFAULT_SLOWDOWN}; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -43,10 +46,27 @@ async fn main() -> std::io::Result<()> { message = "Shipping service is running" ); - HttpServer::new(|| { + let host = env::var("FLAGD_HOST").unwrap_or_else(|_| "flagd".to_string()); + let flagd_port = env::var("FLAGD_OFREP_PORT") + .ok() + .and_then(|p| p.parse::().ok()) + .unwrap_or(8016); + + let provider = FlagdProvider::new(FlagdOptions { + host, + port: flagd_port, + resolver_type: ResolverType::Rest, + ..Default::default() + }) + .await + .expect("Failed to initialize flagd provider"); + + let flag_provider = web::Data::from(Arc::new(provider) as Arc); + + HttpServer::new(move || { App::new() - .app_data(actix_web::web::Data::new(FlagChecker::from_env())) - .app_data(actix_web::web::Data::new(DEFAULT_SLOWDOWN)) + .app_data(flag_provider.clone()) + .app_data(web::Data::new(DEFAULT_SLOWDOWN)) .wrap(RequestTracing::new()) .wrap(RequestMetrics::default()) .service(get_quote) diff --git a/src/shipping/src/shipping_service.rs b/src/shipping/src/shipping_service.rs index 9b0ff1b150..938c26addc 100644 --- a/src/shipping/src/shipping_service.rs +++ b/src/shipping/src/shipping_service.rs @@ -2,12 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use actix_web::{post, web, HttpResponse, Responder}; -use serde::de::DeserializeOwned; -use std::any::Any; -use tracing::info; - -mod feature_flag; -use feature_flag::FlagdClient; +use open_feature::provider::FeatureProvider; +use open_feature::EvaluationContext; +use tracing::{info, warn}; mod quote; use quote::create_quote_from_count; @@ -18,46 +15,6 @@ use tracking::create_tracking_id; mod shipping_types; pub use shipping_types::*; -pub enum FlagChecker { - Real(FlagdClient), - #[cfg(test)] - Mock(std::collections::HashMap>), -} - -impl FlagChecker { - pub fn from_env() -> Self { - Self::Real(FlagdClient::from_env()) - } - - #[cfg(test)] - fn mock() -> Self { - Self::Mock(std::collections::HashMap::new()) - } - - #[cfg(test)] - fn with(mut self, flag: &str, value: impl Any + Send + Sync + 'static) -> Self { - if let Self::Mock(ref mut map) = self { - map.insert(flag.to_string(), Box::new(value) as Box); - } - self - } - - pub async fn evaluate( - &self, - flag_name: &str, - ) -> T { - match self { - Self::Real(client) => client.evaluate(flag_name).await, - #[cfg(test)] - Self::Mock(flags) => flags - .get(flag_name) - .and_then(|v| v.downcast_ref::()) - .cloned() - .unwrap_or_default(), - } - } -} - const NANOS_MULTIPLE: u32 = 10000000u32; pub const DEFAULT_SLOWDOWN: std::time::Duration = std::time::Duration::from_secs(10); @@ -93,7 +50,7 @@ pub async fn get_quote(req: web::Json) -> impl Responder { #[post("/ship-order")] pub async fn ship_order( req: web::Json, - flag_checker: web::Data, + flag_provider: web::Data, slowdown: web::Data, ) -> impl Responder { let is_outside_us = req @@ -107,7 +64,25 @@ pub async fn ship_order( }) .unwrap_or(false); - if is_outside_us && flag_checker.evaluate::("shippingSlowdown").await { + let context = EvaluationContext::default(); + let slowdown_enabled = flag_provider + .resolve_bool_value("shippingSlowdown", &context) + .await + .map(|res| { + info!( + feature_flag.key = "shippingSlowdown", + feature_flag.provider_name = "flagd", + feature_flag.variant = res.variant.as_deref().unwrap_or("unknown"), + "feature flag evaluated" + ); + res.value + }) + .unwrap_or_else(|e| { + warn!("Failed to evaluate feature flag shippingSlowdown: {:?}", e); + false + }); + + if is_outside_us && slowdown_enabled { info!( name = "ShippingSlowdown", message = "Delaying international shipment due to shippingSlowdown feature flag" @@ -127,18 +102,26 @@ pub async fn ship_order( #[cfg(test)] mod tests { use actix_web::{http::header::ContentType, test, App}; + use open_feature::provider::{MockFeatureProvider, ResolutionDetails}; + use std::sync::Arc; use super::*; + fn mock_provider(value: bool) -> web::Data { + let mut mock = MockFeatureProvider::new(); + mock.expect_resolve_bool_value() + .returning(move |_, _| Ok(ResolutionDetails::new(value))); + web::Data::from(Arc::new(mock) as Arc) + } + async fn call_ship_order( address: Option
, - checker: FlagChecker, + provider: web::Data, slowdown: std::time::Duration, ) -> ShipOrderResponse { - let checker = web::Data::new(checker); let app = test::init_service( App::new() - .app_data(checker) + .app_data(provider) .app_data(web::Data::new(slowdown)) .service(ship_order), ) @@ -165,19 +148,19 @@ mod tests { #[actix_web::test] async fn test_ship_order_no_address() { - let order = call_ship_order(None, FlagChecker::mock(), std::time::Duration::ZERO).await; + let order = call_ship_order(None, mock_provider(false), std::time::Duration::ZERO).await; assert!(!order.tracking_id.is_empty()); } #[actix_web::test] async fn test_ship_order_us_address() { - let order = call_ship_order(Some(make_address("US")), FlagChecker::mock(), std::time::Duration::ZERO).await; + let order = call_ship_order(Some(make_address("US")), mock_provider(false), std::time::Duration::ZERO).await; assert!(!order.tracking_id.is_empty()); } #[actix_web::test] async fn test_ship_order_international_flag_off() { - let order = call_ship_order(Some(make_address("CA")), FlagChecker::mock(), std::time::Duration::ZERO).await; + let order = call_ship_order(Some(make_address("CA")), mock_provider(false), std::time::Duration::ZERO).await; assert!(!order.tracking_id.is_empty()); } @@ -185,8 +168,7 @@ mod tests { async fn test_ship_order_international_flag_on() { let slowdown = std::time::Duration::from_millis(50); let start = std::time::Instant::now(); - let checker = FlagChecker::mock().with("shippingSlowdown", true); - let order = call_ship_order(Some(make_address("CA")), checker, slowdown).await; + let order = call_ship_order(Some(make_address("CA")), mock_provider(true), slowdown).await; assert!(start.elapsed() >= slowdown); assert!(!order.tracking_id.is_empty()); } @@ -194,9 +176,8 @@ mod tests { #[actix_web::test] async fn test_ship_order_us_flag_on() { let slowdown = std::time::Duration::from_millis(50); - let checker = FlagChecker::mock().with("shippingSlowdown", true); let start = std::time::Instant::now(); - let order = call_ship_order(Some(make_address("US")), checker, slowdown).await; + let order = call_ship_order(Some(make_address("US")), mock_provider(true), slowdown).await; assert!(start.elapsed() < slowdown); assert!(!order.tracking_id.is_empty()); } diff --git a/src/shipping/src/shipping_service/feature_flag.rs b/src/shipping/src/shipping_service/feature_flag.rs deleted file mode 100644 index c91bfe1bed..0000000000 --- a/src/shipping/src/shipping_service/feature_flag.rs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -use awc::Client; -use opentelemetry_instrumentation_actix_web::ClientExt; -use serde::de::DeserializeOwned; -use serde::Deserialize; -use std::cell::RefCell; -use std::env; -use tracing::{info, warn}; - -thread_local! { - static HTTP_CLIENT: RefCell> = const { RefCell::new(None) }; -} - -fn get_client() -> Client { - HTTP_CLIENT.with(|cell| { - let mut opt = cell.borrow_mut(); - if opt.is_none() { - *opt = Some(Client::default()); - } - opt.as_ref().unwrap().clone() - }) -} - -#[derive(Debug, Deserialize)] -struct OFREPResponse { - value: T, - variant: Option, -} - -#[derive(Clone)] -pub struct FlagdClient { - base_url: String, -} - -impl FlagdClient { - pub fn from_env() -> Self { - let host = env::var("FLAGD_HOST").unwrap_or_else(|_| "flagd".to_string()); - let port = env::var("FLAGD_OFREP_PORT") - .ok() - .and_then(|p| p.parse::().ok()) - .unwrap_or(8016); - - Self { - base_url: format!("http://{}:{}/ofrep/v1/evaluate/flags", host, port), - } - } - - pub async fn evaluate(&self, flag_name: &str) -> T { - let url = format!("{}/{}", self.base_url, flag_name); - - let result = get_client() - .post(&url) - .insert_header(("Content-Type", "application/json")) - .trace_request() - .send_body(r#"{"context":{}}"#) - .await; - - match result { - Ok(mut resp) if resp.status().is_success() => { - match resp.json::>().await { - Ok(data) => { - info!( - feature_flag.key = flag_name, - feature_flag.provider_name = "flagd", - feature_flag.variant = data.variant.as_deref().unwrap_or("unknown"), - "feature flag evaluated" - ); - data.value - } - Err(e) => { - warn!("Failed to parse feature flag response for {}: {}", flag_name, e); - T::default() - } - } - } - Ok(resp) => { - warn!("Feature flag {} returned HTTP {}", flag_name, resp.status()); - T::default() - } - Err(e) => { - warn!("Failed to check feature flag {}: {}", flag_name, e); - T::default() - } - } - } - -} From 9450555074e29202c7c34e84ef87a05bb7625fb2 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Thu, 14 May 2026 22:16:30 -0400 Subject: [PATCH 22/26] refactor(shipping): switch flagd from REST/OFREP to gRPC Use the crate's default gRPC resolver (port 8013) instead of REST/OFREP (port 8016). Update docker-compose files to pass FLAGD_PORT instead of FLAGD_OFREP_PORT to the shipping service. Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- docker-compose.minimal.yml | 2 +- docker-compose.yml | 2 +- src/shipping/src/main.rs | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docker-compose.minimal.yml b/docker-compose.minimal.yml index 7a90b709e6..cd15888760 100644 --- a/docker-compose.minimal.yml +++ b/docker-compose.minimal.yml @@ -563,7 +563,7 @@ services: - SHIPPING_PORT - QUOTE_ADDR - FLAGD_HOST - - FLAGD_OFREP_PORT + - FLAGD_PORT - OTEL_EXPORTER_OTLP_ENDPOINT - OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES},service.criticality=high - OTEL_SERVICE_NAME=shipping diff --git a/docker-compose.yml b/docker-compose.yml index c0cea7c545..1a4a5ec383 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -650,7 +650,7 @@ services: - SHIPPING_PORT - QUOTE_ADDR - FLAGD_HOST - - FLAGD_OFREP_PORT + - FLAGD_PORT - OTEL_EXPORTER_OTLP_ENDPOINT - OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES},service.criticality=high - OTEL_SERVICE_NAME=shipping diff --git a/src/shipping/src/main.rs b/src/shipping/src/main.rs index 0105a8b543..28b090f945 100644 --- a/src/shipping/src/main.rs +++ b/src/shipping/src/main.rs @@ -3,7 +3,7 @@ use actix_web::{web, App, HttpServer}; use open_feature::provider::FeatureProvider; -use open_feature_flagd::{FlagdOptions, FlagdProvider, ResolverType}; +use open_feature_flagd::{FlagdOptions, FlagdProvider}; use opentelemetry_instrumentation_actix_web::{RequestMetrics, RequestTracing}; use std::env; use std::sync::Arc; @@ -47,15 +47,14 @@ async fn main() -> std::io::Result<()> { ); let host = env::var("FLAGD_HOST").unwrap_or_else(|_| "flagd".to_string()); - let flagd_port = env::var("FLAGD_OFREP_PORT") + let flagd_port = env::var("FLAGD_PORT") .ok() .and_then(|p| p.parse::().ok()) - .unwrap_or(8016); + .unwrap_or(8013); let provider = FlagdProvider::new(FlagdOptions { host, port: flagd_port, - resolver_type: ResolverType::Rest, ..Default::default() }) .await From d93c9b2c5f1e4ba3ff8ce2f64b8f473c8fd777e8 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Thu, 14 May 2026 23:04:17 -0400 Subject: [PATCH 23/26] fix(shipping): disable FlagdProvider cache so flag changes are picked up immediately The default RPC resolver wraps evaluations in an LRU cache with a 60s TTL, causing demo.flagd.json edits to be invisible until the cache expires. Setting cache_settings: None bypasses the cache and makes every evaluation a fresh gRPC unary call, matching the behaviour of the old OFREP client. Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shipping/src/main.rs b/src/shipping/src/main.rs index 28b090f945..f31cee01b4 100644 --- a/src/shipping/src/main.rs +++ b/src/shipping/src/main.rs @@ -55,6 +55,7 @@ async fn main() -> std::io::Result<()> { let provider = FlagdProvider::new(FlagdOptions { host, port: flagd_port, + cache_settings: None, ..Default::default() }) .await From 3de2ef07190a9b453eaabf13e8cf91faac97494d Mon Sep 17 00:00:00 2001 From: tlee439 Date: Thu, 14 May 2026 23:15:37 -0400 Subject: [PATCH 24/26] cleanup(shipping): trim flagd provider deps and redundant env reads Narrow open-feature-flagd to the rpc feature only (drops reqwest, datalogic-rs, notify, semver, etc.). Remove manual FLAGD_HOST/PORT reads since FlagdOptions::default() already reads them from env. Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/Cargo.lock | 481 --------------------------------------- src/shipping/Cargo.toml | 2 +- src/shipping/src/main.rs | 8 - 3 files changed, 1 insertion(+), 490 deletions(-) diff --git a/src/shipping/Cargo.lock b/src/shipping/Cargo.lock index 1f418ca5b5..8ab8b5130f 100644 --- a/src/shipping/Cargo.lock +++ b/src/shipping/Cargo.lock @@ -240,15 +240,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstyle" version = "1.0.14" @@ -278,12 +269,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "awc" version = "3.8.2" @@ -440,12 +425,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chacha20" version = "0.10.0" @@ -457,17 +436,6 @@ dependencies = [ "rand_core 0.10.0", ] -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "num-traits", - "windows-link", -] - [[package]] name = "const-oid" version = "0.10.2" @@ -494,12 +462,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "cpufeatures" version = "0.3.0" @@ -527,18 +489,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "datalogic-rs" -version = "4.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcab7239cbd842e6592ff150b568b4274700de080ff3c3060d80281f47d7c27a" -dependencies = [ - "chrono", - "regex", - "serde", - "serde_json", -] - [[package]] name = "deranged" version = "0.5.8" @@ -694,15 +644,6 @@ dependencies = [ "futures-core", ] -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - [[package]] name = "futures-channel" version = "0.3.32" @@ -775,19 +716,6 @@ dependencies = [ "slab", ] -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - [[package]] name = "getrandom" version = "0.3.4" @@ -795,11 +723,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasip2", - "wasm-bindgen", ] [[package]] @@ -974,22 +900,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http 1.3.1", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - [[package]] name = "hyper-timeout" version = "0.5.2" @@ -1026,30 +936,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "2.2.0" @@ -1177,26 +1063,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "inotify" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" -dependencies = [ - "bitflags", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -1248,26 +1114,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" -dependencies = [ - "bitflags", - "libc", -] - [[package]] name = "language-tags" version = "0.3.2" @@ -1345,12 +1191,6 @@ dependencies = [ "hashbrown 0.16.1", ] -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "matchers" version = "0.2.0" @@ -1432,39 +1272,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -[[package]] -name = "murmurhash3" -version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2983372caf4480544083767bf2d27defafe32af49ab4df3a0b7fc90793a3664" - -[[package]] -name = "notify" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" -dependencies = [ - "bitflags", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - -[[package]] -name = "notify-types" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" -dependencies = [ - "bitflags", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1480,15 +1287,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -1517,16 +1315,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6539a78daae309c8f18dd57f6e20dfdbb5c227dd5737425549c54c303688a065" dependencies = [ "async-trait", - "datalogic-rs", "hyper-util", "lru", - "murmurhash3", - "notify", "open-feature", "prost", "prost-types", - "reqwest", - "semver", "serde", "serde_json", "thiserror", @@ -1871,61 +1664,6 @@ dependencies = [ "pulldown-cmark", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2 0.6.1", - "thiserror", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.1", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.45" @@ -2046,53 +1784,25 @@ dependencies = [ "http-body", "http-body-util", "hyper", - "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", - "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", "web-sys", - "webpki-roots", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", ] -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - [[package]] name = "rustc_version" version = "0.4.1" @@ -2115,41 +1825,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -2162,15 +1837,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -2340,12 +2006,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "2.0.117" @@ -2466,21 +2126,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.50.0" @@ -2509,16 +2154,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.18" @@ -2778,12 +2413,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" version = "2.5.8" @@ -2826,16 +2455,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -2949,19 +2568,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "wasmparser" version = "0.244.0" @@ -2984,93 +2590,12 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -3391,12 +2916,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - [[package]] name = "zerotrie" version = "0.2.4" diff --git a/src/shipping/Cargo.toml b/src/shipping/Cargo.toml index 369f6a183c..1176eb6db3 100644 --- a/src/shipping/Cargo.toml +++ b/src/shipping/Cargo.toml @@ -14,7 +14,7 @@ actix-web = "4" anyhow = "1.0.102" awc = { version = "3.8.2", default-features = false, features = ["compress-zstd"] } open-feature = "0.2" -open-feature-flagd = "0.1" +open-feature-flagd = { version = "0.1", default-features = false, features = ["rpc"] } serde = { version = "1.0.228", features = ["derive"] } tonic = "0.14.5" tracing = "0.1.44" diff --git a/src/shipping/src/main.rs b/src/shipping/src/main.rs index f31cee01b4..b9001f4732 100644 --- a/src/shipping/src/main.rs +++ b/src/shipping/src/main.rs @@ -46,15 +46,7 @@ async fn main() -> std::io::Result<()> { message = "Shipping service is running" ); - let host = env::var("FLAGD_HOST").unwrap_or_else(|_| "flagd".to_string()); - let flagd_port = env::var("FLAGD_PORT") - .ok() - .and_then(|p| p.parse::().ok()) - .unwrap_or(8013); - let provider = FlagdProvider::new(FlagdOptions { - host, - port: flagd_port, cache_settings: None, ..Default::default() }) From 3501c1c107fee24f78289de88a5a0c3949ec63f6 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Thu, 14 May 2026 23:26:05 -0400 Subject: [PATCH 25/26] perf(shipping): skip flag evaluation for US addresses Short-circuit the && so the gRPC call to flagd only happens when the address is outside the US. Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- src/shipping/src/shipping_service.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/shipping/src/shipping_service.rs b/src/shipping/src/shipping_service.rs index 938c26addc..1a3e4698c2 100644 --- a/src/shipping/src/shipping_service.rs +++ b/src/shipping/src/shipping_service.rs @@ -64,9 +64,8 @@ pub async fn ship_order( }) .unwrap_or(false); - let context = EvaluationContext::default(); - let slowdown_enabled = flag_provider - .resolve_bool_value("shippingSlowdown", &context) + if is_outside_us && flag_provider + .resolve_bool_value("shippingSlowdown", &EvaluationContext::default()) .await .map(|res| { info!( @@ -80,9 +79,8 @@ pub async fn ship_order( .unwrap_or_else(|e| { warn!("Failed to evaluate feature flag shippingSlowdown: {:?}", e); false - }); - - if is_outside_us && slowdown_enabled { + }) + { info!( name = "ShippingSlowdown", message = "Delaying international shipment due to shippingSlowdown feature flag" From dc701cc1ee4761a7f1c54903bd36156f5a5612d5 Mon Sep 17 00:00:00 2001 From: tlee439 Date: Tue, 19 May 2026 16:18:28 -0400 Subject: [PATCH 26/26] refactor(shipping): replace boolean shippingSlowdown with integer intlShippingSlowdown flag The flag value itself now carries the delay in seconds (default 0), removing the need for DEFAULT_SLOWDOWN and the injected duration parameter. Assisted-by: This contribution was prepared with the assistance of an AI development tool. --- CHANGELOG.md | 4 +- src/flagd/demo.flagd.json | 7 +-- src/shipping/README.md | 6 +-- src/shipping/src/main.rs | 3 +- src/shipping/src/shipping_service.rs | 70 ++++++++++++++-------------- 5 files changed, 45 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70efe4078c..959233f3a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,8 @@ the release. ## Unreleased -* [shipping] Add `shippingSlowdown` feature flag to delay international (non-US) - shipments via flagd OFREP integration with OpenTelemetry tracing +* [shipping] Add `intlShippingSlowdown` integer feature flag to delay + international (non-US) shipments by N seconds via flagd with OpenTelemetry tracing ([#346](https://github.com/open-telemetry/opentelemetry-demo/issues/346)) * [telemetry-docs] Add a new service to provide telemetry documentation based on Weaver diff --git a/src/flagd/demo.flagd.json b/src/flagd/demo.flagd.json index b78efa00a9..38b22e51f6 100644 --- a/src/flagd/demo.flagd.json +++ b/src/flagd/demo.flagd.json @@ -147,12 +147,13 @@ }, "defaultVariant": "off" }, - "shippingSlowdown": { + "intlShippingSlowdown": { "description": "Delays international shipping responses to simulate overseas shipping delay", "state": "ENABLED", "variants": { - "on": true, - "off": false + "10sec": 10, + "5sec": 5, + "off": 0 }, "defaultVariant": "off" } diff --git a/src/shipping/README.md b/src/shipping/README.md index de473156e6..4253312005 100644 --- a/src/shipping/README.md +++ b/src/shipping/README.md @@ -24,9 +24,9 @@ cargo test ## Feature Flags -* `shippingSlowdown`: when enabled, non-US shipping requests are delayed by 10 - seconds to simulate overseas shipping latency. US addresses are never affected. - The flag is evaluated via the flagd OFREP REST API. +* `intlShippingSlowdown`: integer flag (default 0). When non-zero, non-US + shipping requests are delayed by the flag's value in seconds to simulate + overseas shipping latency. US addresses are never affected. ## Environment Variables diff --git a/src/shipping/src/main.rs b/src/shipping/src/main.rs index b9001f4732..2b9b3eb7c3 100644 --- a/src/shipping/src/main.rs +++ b/src/shipping/src/main.rs @@ -12,7 +12,7 @@ use tracing::info; mod telemetry_conf; use telemetry_conf::init_otel; mod shipping_service; -use shipping_service::{get_quote, ship_order, DEFAULT_SLOWDOWN}; +use shipping_service::{get_quote, ship_order}; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -58,7 +58,6 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { App::new() .app_data(flag_provider.clone()) - .app_data(web::Data::new(DEFAULT_SLOWDOWN)) .wrap(RequestTracing::new()) .wrap(RequestMetrics::default()) .service(get_quote) diff --git a/src/shipping/src/shipping_service.rs b/src/shipping/src/shipping_service.rs index 1a3e4698c2..d112da7e6d 100644 --- a/src/shipping/src/shipping_service.rs +++ b/src/shipping/src/shipping_service.rs @@ -16,7 +16,6 @@ mod shipping_types; pub use shipping_types::*; const NANOS_MULTIPLE: u32 = 10000000u32; -pub const DEFAULT_SLOWDOWN: std::time::Duration = std::time::Duration::from_secs(10); #[post("/get-quote")] pub async fn get_quote(req: web::Json) -> impl Responder { @@ -51,7 +50,6 @@ pub async fn get_quote(req: web::Json) -> impl Responder { pub async fn ship_order( req: web::Json, flag_provider: web::Data, - slowdown: web::Data, ) -> impl Responder { let is_outside_us = req .address @@ -64,28 +62,34 @@ pub async fn ship_order( }) .unwrap_or(false); - if is_outside_us && flag_provider - .resolve_bool_value("shippingSlowdown", &EvaluationContext::default()) - .await - .map(|res| { - info!( - feature_flag.key = "shippingSlowdown", - feature_flag.provider_name = "flagd", - feature_flag.variant = res.variant.as_deref().unwrap_or("unknown"), - "feature flag evaluated" - ); - res.value - }) - .unwrap_or_else(|e| { - warn!("Failed to evaluate feature flag shippingSlowdown: {:?}", e); - false - }) - { + let slowdown_secs = if is_outside_us { + flag_provider + .resolve_int_value("intlShippingSlowdown", &EvaluationContext::default()) + .await + .map(|res| { + info!( + feature_flag.key = "intlShippingSlowdown", + feature_flag.provider_name = "flagd", + feature_flag.variant = res.variant.as_deref().unwrap_or("unknown"), + message = "feature flag evaluated" + ); + res.value + }) + .unwrap_or_else(|e| { + warn!("Failed to evaluate feature flag intlShippingSlowdown: {:?}", e); + 0 + }) + } else { + 0 + }; + + if slowdown_secs > 0 { info!( - name = "ShippingSlowdown", - message = "Delaying international shipment due to shippingSlowdown feature flag" + name = "IntlShippingSlowdown", + shipping.delay_secs = slowdown_secs, + message = "Delaying international shipment due to intlShippingSlowdown feature flag" ); - actix_web::rt::time::sleep(**slowdown).await; + actix_web::rt::time::sleep(std::time::Duration::from_secs(slowdown_secs as u64)).await; } let tid = create_tracking_id(); @@ -105,9 +109,9 @@ mod tests { use super::*; - fn mock_provider(value: bool) -> web::Data { + fn mock_provider(value: i64) -> web::Data { let mut mock = MockFeatureProvider::new(); - mock.expect_resolve_bool_value() + mock.expect_resolve_int_value() .returning(move |_, _| Ok(ResolutionDetails::new(value))); web::Data::from(Arc::new(mock) as Arc) } @@ -115,12 +119,10 @@ mod tests { async fn call_ship_order( address: Option
, provider: web::Data, - slowdown: std::time::Duration, ) -> ShipOrderResponse { let app = test::init_service( App::new() .app_data(provider) - .app_data(web::Data::new(slowdown)) .service(ship_order), ) .await; @@ -146,37 +148,35 @@ mod tests { #[actix_web::test] async fn test_ship_order_no_address() { - let order = call_ship_order(None, mock_provider(false), std::time::Duration::ZERO).await; + let order = call_ship_order(None, mock_provider(0)).await; assert!(!order.tracking_id.is_empty()); } #[actix_web::test] async fn test_ship_order_us_address() { - let order = call_ship_order(Some(make_address("US")), mock_provider(false), std::time::Duration::ZERO).await; + let order = call_ship_order(Some(make_address("US")), mock_provider(0)).await; assert!(!order.tracking_id.is_empty()); } #[actix_web::test] async fn test_ship_order_international_flag_off() { - let order = call_ship_order(Some(make_address("CA")), mock_provider(false), std::time::Duration::ZERO).await; + let order = call_ship_order(Some(make_address("FR")), mock_provider(0)).await; assert!(!order.tracking_id.is_empty()); } #[actix_web::test] async fn test_ship_order_international_flag_on() { - let slowdown = std::time::Duration::from_millis(50); let start = std::time::Instant::now(); - let order = call_ship_order(Some(make_address("CA")), mock_provider(true), slowdown).await; - assert!(start.elapsed() >= slowdown); + let order = call_ship_order(Some(make_address("FR")), mock_provider(1)).await; + assert!(start.elapsed() >= std::time::Duration::from_secs(1)); assert!(!order.tracking_id.is_empty()); } #[actix_web::test] async fn test_ship_order_us_flag_on() { - let slowdown = std::time::Duration::from_millis(50); let start = std::time::Instant::now(); - let order = call_ship_order(Some(make_address("US")), mock_provider(true), slowdown).await; - assert!(start.elapsed() < slowdown); + let order = call_ship_order(Some(make_address("US")), mock_provider(10)).await; + assert!(start.elapsed() < std::time::Duration::from_secs(1)); assert!(!order.tracking_id.is_empty()); } }