diff --git a/CHANGELOG.md b/CHANGELOG.md index d8044721f2..ae36b004ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ the release. ## Unreleased +* [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 ([#2794](https://github.com/open-telemetry/opentelemetry-demo/pull/2794)) diff --git a/docker-compose.minimal.yml b/docker-compose.minimal.yml index 05e4d28431..b8f8b903f4 100644 --- a/docker-compose.minimal.yml +++ b/docker-compose.minimal.yml @@ -564,6 +564,8 @@ services: - IPV6_ENABLED - SHIPPING_PORT - QUOTE_ADDR + - FLAGD_HOST + - FLAGD_PORT - OTEL_EXPORTER_OTLP_ENDPOINT - OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES},service.criticality=high - OTEL_SERVICE_NAME=shipping @@ -577,6 +579,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 b2a86a7c8b..50e4d001d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -651,6 +651,8 @@ services: - IPV6_ENABLED - SHIPPING_PORT - QUOTE_ADDR + - FLAGD_HOST + - FLAGD_PORT - OTEL_EXPORTER_OTLP_ENDPOINT - OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES},service.criticality=high - OTEL_SERVICE_NAME=shipping @@ -658,6 +660,8 @@ services: depends_on: otel-collector: condition: service_started + flagd: + condition: service_started logging: *logging # ****************** diff --git a/src/flagd/demo.flagd.json b/src/flagd/demo.flagd.json index 44864130c9..38b22e51f6 100644 --- a/src/flagd/demo.flagd.json +++ b/src/flagd/demo.flagd.json @@ -146,6 +146,16 @@ "10000x": 10000 }, "defaultVariant": "off" + }, + "intlShippingSlowdown": { + "description": "Delays international shipping responses to simulate overseas shipping delay", + "state": "ENABLED", + "variants": { + "10sec": 10, + "5sec": 5, + "off": 0 + }, + "defaultVariant": "off" } } } diff --git a/src/shipping/Cargo.lock b/src/shipping/Cargo.lock index 5eb98114e9..2be620917f 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,18 @@ 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 = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "anyhow" version = "1.0.102" @@ -531,6 +543,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" @@ -562,12 +580,24 @@ dependencies = [ "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 +620,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 +635,15 @@ 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 = "futures-channel" version = "0.3.32" @@ -741,7 +786,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]] @@ -874,7 +930,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -1072,6 +1128,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" @@ -1110,6 +1172,15 @@ 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 = "matchers" version = "0.2.0" @@ -1159,6 +1230,38 @@ 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 = "nu-ansi-term" version = "0.50.3" @@ -1180,6 +1283,45 @@ 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", + "hyper-util", + "lru", + "open-feature", + "prost", + "prost-types", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tower", + "tracing", +] + [[package]] name = "opentelemetry" version = "0.32.0" @@ -1329,6 +1471,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.13" @@ -1391,6 +1544,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" @@ -1420,6 +1599,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" @@ -1442,6 +1642,26 @@ 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 = "quote" version = "1.0.45" @@ -1587,6 +1807,19 @@ 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 = "rustversion" version = "1.0.22" @@ -1693,6 +1926,8 @@ dependencies = [ "actix-web", "anyhow", "awc", + "open-feature", + "open-feature-flagd", "opentelemetry", "opentelemetry-appender-tracing", "opentelemetry-instrumentation-actix-web", @@ -1797,6 +2032,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" @@ -1948,6 +2202,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.6" @@ -1959,6 +2225,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 = "tonic-types" version = "0.14.6" @@ -2087,12 +2369,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" diff --git a/src/shipping/Cargo.toml b/src/shipping/Cargo.toml index 2935a79b22..50698fd4a6 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 = { version = "0.1", default-features = false, features = ["rpc"] } serde = { version = "1.0.228", features = ["derive"] } tonic = "0.14.6" tracing = "0.1.44" diff --git a/src/shipping/README.md b/src/shipping/README.md index 915a78aba3..4253312005 100644 --- a/src/shipping/README.md +++ b/src/shipping/README.md @@ -21,3 +21,16 @@ docker compose build shipping ```sh cargo test ``` + +## Feature Flags + +* `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 + +| Variable | Default | Description | +|--------------------|---------|-------------------------------| +| `FLAGD_HOST` | `flagd` | Hostname of the flagd service | +| `FLAGD_OFREP_PORT` | `8016` | Port for the flagd OFREP API | diff --git a/src/shipping/src/main.rs b/src/shipping/src/main.rs index 102bcbd9dd..2b9b3eb7c3 100644 --- a/src/shipping/src/main.rs +++ b/src/shipping/src/main.rs @@ -1,9 +1,12 @@ // 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}; use opentelemetry_instrumentation_actix_web::{RequestMetrics, RequestTracing}; use std::env; +use std::sync::Arc; use tracing::info; mod telemetry_conf; @@ -43,8 +46,18 @@ async fn main() -> std::io::Result<()> { message = "Shipping service is running" ); - HttpServer::new(|| { + let provider = FlagdProvider::new(FlagdOptions { + cache_settings: None, + ..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(flag_provider.clone()) .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 5dd3bc75d3..d112da7e6d 100644 --- a/src/shipping/src/shipping_service.rs +++ b/src/shipping/src/shipping_service.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use actix_web::{post, web, HttpResponse, Responder}; -use tracing::info; +use open_feature::provider::FeatureProvider; +use open_feature::EvaluationContext; +use tracing::{info, warn}; mod quote; use quote::create_quote_from_count; @@ -45,7 +47,51 @@ 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_provider: web::Data, +) -> impl Responder { + let is_outside_us = req + .address + .as_ref() + .map(|addr| { + !matches!( + addr.country.to_uppercase().trim(), + "US" | "USA" | "UNITED STATES" | "UNITED STATES OF AMERICA" + ) + }) + .unwrap_or(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 = "IntlShippingSlowdown", + shipping.delay_secs = slowdown_secs, + message = "Delaying international shipment due to intlShippingSlowdown feature flag" + ); + actix_web::rt::time::sleep(std::time::Duration::from_secs(slowdown_secs as u64)).await; + } + let tid = create_tracking_id(); info!( name = "CreatingTrackingId", @@ -58,24 +104,79 @@ pub async fn ship_order(_req: web::Json) -> impl Responder { #[cfg(test)] mod tests { use actix_web::{http::header::ContentType, test, App}; + use open_feature::provider::{MockFeatureProvider, ResolutionDetails}; + use std::sync::Arc; use super::*; - #[actix_web::test] - async fn test_ship_order() { - let app = test::init_service(App::new().service(ship_order)).await; + fn mock_provider(value: i64) -> web::Data { + let mut mock = MockFeatureProvider::new(); + mock.expect_resolve_int_value() + .returning(move |_, _| Ok(ResolutionDetails::new(value))); + web::Data::from(Arc::new(mock) as Arc) + } + + async fn call_ship_order( + address: Option
, + provider: web::Data, + ) -> ShipOrderResponse { + let app = test::init_service( + App::new() + .app_data(provider) + .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, 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(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("FR")), mock_provider(0)).await; + assert!(!order.tracking_id.is_empty()); + } + + #[actix_web::test] + async fn test_ship_order_international_flag_on() { + let start = std::time::Instant::now(); + 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()); + } - let order: ShipOrderResponse = test::read_body_json(resp).await; + #[actix_web::test] + async fn test_ship_order_us_flag_on() { + let start = std::time::Instant::now(); + 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()); } }