diff --git a/.changesets/breaking_tninesling_context_keys.md b/.changesets/breaking_tninesling_context_keys.md new file mode 100644 index 00000000000..5784bb2afd8 --- /dev/null +++ b/.changesets/breaking_tninesling_context_keys.md @@ -0,0 +1,11 @@ +### Update context key names for consistency and document them ([PR #6572](https://github.com/apollographql/router/pull/6572)) + +Documentation and naming refactor for context keys. The new unified taxonomy for context keys is formatted at `apollo::` + router stage or plugin name + `::` + key descriptor. + +Selected examples: +`operation_name` -> `apollo::supergraph::operation_name` +`apollo_authentication::JWT::claims` -> `apollo::authentication::jwt_claims` + +A full compendium of available context keys in this new format can be found in the request lifecycle section of [docs/source/routing/customization/overview.mdx](https://github.com/apollographql/router/blob/tninesling/context-docs/docs/source/routing/customization/overview.mdx#request-context). + +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/6572 diff --git a/Cargo.lock b/Cargo.lock index 6170ab4a35f..ab86268b930 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,9 +168,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "apollo-compiler" @@ -181,7 +181,7 @@ dependencies = [ "ahash", "apollo-parser", "ariadne", - "indexmap 2.7.1", + "indexmap 2.7.0", "rowan", "serde", "serde_json_bytes", @@ -212,7 +212,7 @@ dependencies = [ "hashbrown 0.15.2", "hex", "http 1.2.0", - "indexmap 2.7.1", + "indexmap 2.7.0", "insta", "itertools 0.13.0", "line-col", @@ -320,11 +320,11 @@ dependencies = [ "http-serde", "humantime", "humantime-serde", - "hyper 1.5.2", + "hyper 1.5.1", "hyper-rustls 0.27.3", "hyper-util", "hyperlocal", - "indexmap 2.7.1", + "indexmap 2.7.0", "insta", "itertools 0.13.0", "itoa", @@ -459,7 +459,7 @@ dependencies = [ "apollo-compiler", "apollo-parser", "arbitrary", - "indexmap 2.7.1", + "indexmap 2.7.0", "once_cell", "thiserror 1.0.69", ] @@ -607,7 +607,7 @@ dependencies = [ "futures-util", "handlebars", "http 0.2.12", - "indexmap 2.7.1", + "indexmap 2.7.0", "mime", "multer 2.1.0", "num-traits", @@ -676,7 +676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "323a5143f5bdd2030f45e3f2e0c821c9b1d36e79cf382129c64299c50a7f3750" dependencies = [ "bytes", - "indexmap 2.7.1", + "indexmap 2.7.0", "serde", "serde_json", ] @@ -828,9 +828,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-config" -version = "1.5.14" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f40e82e858e02445402906e454a73e244c7f501fcae198977585946c48e8697" +checksum = "4e95816a168520d72c0e7680c405a5a8c1fb6a035b4bc4b9d7b0de8e1a941697" dependencies = [ "aws-credential-types", "aws-runtime", @@ -896,9 +896,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.4" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee7643696e7fdd74c10f9eb42848a87fe469d35eae9c3323f80aa98f350baac" +checksum = "b5ac934720fbb46206292d2c75b57e67acfc56fe7dfd34fb9a02334af08409ea" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -921,9 +921,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.54.0" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921a13ed6aabe2d1258f65ef7804946255c799224440774c30e1a2c65cdf983a" +checksum = "11822090cf501c316c6f75711d77b96fba30658e3867a7762e5e2f5d32d31e81" dependencies = [ "aws-credential-types", "aws-runtime", @@ -943,9 +943,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.55.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196c952738b05dfc917d82a3e9b5ba850822a6d6a86d677afda2a156cc172ceb" +checksum = "78a2a06ff89176123945d1bbe865603c4d7101bea216a550bb4d2e4e9ba74d74" dependencies = [ "aws-credential-types", "aws-runtime", @@ -965,9 +965,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.55.0" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ef5b73a927ed80b44096f8c20fb4abae65469af15198367e179ae267256e9d" +checksum = "a20a91795850826a6f456f4a48eff1dfa59a0e69bdbf5b8c50518fd372106574" dependencies = [ "aws-credential-types", "aws-runtime", @@ -988,9 +988,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.7" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "690118821e46967b3c4501d67d7d52dd75106a9c54cf36cefa1985cedbe94e05" +checksum = "7d3820e0c08d0737872ff3c7c1f21ebbb6693d832312d6152bf18ef50a5471c2" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -1011,9 +1011,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa59d1327d8b5053c54bf2eaae63bf629ba9e904434d0835a28ed3c0ed0a614e" +checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c" dependencies = [ "futures-util", "pin-project-lite", @@ -1022,9 +1022,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.60.12" +version = "0.60.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7809c27ad8da6a6a68c454e651d4962479e81472aa19ae99e59f9aba1f9713cc" +checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -1042,9 +1042,9 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.61.2" +version = "0.60.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "623a51127f24c30776c8b374295f2df78d92517386f77ba30773f15a30ce1422" +checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" dependencies = [ "aws-smithy-types", ] @@ -1061,9 +1061,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.7.7" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "865f7050bbc7107a6c98a397a9fcd9413690c27fa718446967cf03b2d3ac517e" +checksum = "9f20685047ca9d6f17b994a07f629c813f08b5bce65523e47124879e60103d45" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -1105,9 +1105,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.12" +version = "1.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28f6feb647fb5e0d5b50f0472c19a7db9462b74e2fec01bb0b44eedcc834e97" +checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510" dependencies = [ "base64-simd", "bytes", @@ -1137,9 +1137,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.4" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0df5a18c4f951c645300d365fec53a61418bcf4650f604f85fe2a665bfaa0c2" +checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -1226,7 +1226,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.5.1", "hyper-util", "itoa", "matchit 0.8.4", @@ -1339,7 +1339,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.5.1", "hyper-util", "pin-project-lite", "tokio", @@ -2498,7 +2498,7 @@ dependencies = [ "futures", "http 1.2.0", "http-body-util", - "hyper 1.5.2", + "hyper 1.5.1", "multimap 0.9.1", "schemars", "serde", @@ -3039,7 +3039,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.7.1", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -3058,7 +3058,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.2.0", - "indexmap 2.7.1", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -3439,9 +3439,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -3482,7 +3482,7 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.2.0", - "hyper 1.5.2", + "hyper 1.5.1", "hyper-util", "log", "rustls 0.23.19", @@ -3500,7 +3500,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.5.2", + "hyper 1.5.1", "hyper-util", "pin-project-lite", "tokio", @@ -3518,7 +3518,7 @@ dependencies = [ "futures-util", "http 1.2.0", "http-body 1.0.1", - "hyper 1.5.2", + "hyper 1.5.1", "pin-project-lite", "socket2", "tokio", @@ -3534,7 +3534,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" dependencies = [ "hex", "http-body-util", - "hyper 1.5.2", + "hyper 1.5.1", "hyper-util", "pin-project-lite", "tokio", @@ -3731,9 +3731,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -3768,14 +3768,14 @@ dependencies = [ [[package]] name = "insta" -version = "1.42.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513e4067e16e69ed1db5ab56048ed65db32d10ba5fc1217f5393f8f17d8b5a5" +checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" dependencies = [ "console", "globset", + "lazy_static", "linked-hash-map", - "once_cell", "pest", "pest_derive", "serde", @@ -4633,7 +4633,7 @@ dependencies = [ "ahash", "futures-core", "http 1.2.0", - "indexmap 2.7.1", + "indexmap 2.7.0", "itertools 0.11.0", "itoa", "once_cell", @@ -4966,7 +4966,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.7.1", + "indexmap 2.7.0", "serde", "serde_derive", ] @@ -5619,7 +5619,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.5.1", "hyper-rustls 0.27.3", "hyper-util", "ipnet", @@ -5680,9 +5680,9 @@ dependencies = [ [[package]] name = "rhai" -version = "1.20.1" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0277a46f29fe3b3eb10821ca2c65a4751b686b6c84422aae31695ba167b0fbc" +checksum = "8867cfc57aaf2320b60ec0f4d55603ac950ce852e6ab6b9109aa3d626a4dd7ea" dependencies = [ "ahash", "bitflags 2.6.0", @@ -6209,24 +6209,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.25" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -6258,11 +6258,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.137" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.7.0", "itoa", "memchr", "ryu", @@ -6277,7 +6277,7 @@ checksum = "0ecd92a088fb2500b2f146c9ddc5da9950bb7264d3f00932cd2a6fb369c26c46" dependencies = [ "ahash", "bytes", - "indexmap 2.7.1", + "indexmap 2.7.0", "jsonpath-rust", "regex", "serde", @@ -6378,12 +6378,12 @@ dependencies = [ [[package]] name = "shape" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70179a0773695f4fc0b3e8e59f356064ed1532492e52bcf7e0dfef42934ec4c5" +checksum = "7417fe45205214f2386c2dfbedc2173c4a7d7d2178cae2446b037fc6b89ec7cd" dependencies = [ "apollo-compiler", - "indexmap 2.7.1", + "indexmap 2.7.0", "lazy_static", "serde_json", "serde_json_bytes", @@ -7067,7 +7067,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.7.0", "toml_datetime", "winnow 0.5.40", ] @@ -7078,7 +7078,7 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.7.0", "toml_datetime", "winnow 0.6.20", ] @@ -7120,7 +7120,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.5.1", "hyper-timeout", "hyper-util", "percent-encoding", @@ -7181,7 +7181,7 @@ dependencies = [ "futures-core", "futures-util", "hdrhistogram", - "indexmap 2.7.1", + "indexmap 2.7.0", "pin-project-lite", "slab", "sync_wrapper 1.0.2", diff --git a/apollo-router/src/context/mod.rs b/apollo-router/src/context/mod.rs index daade4ea5ab..68ea1b9a66b 100644 --- a/apollo-router/src/context/mod.rs +++ b/apollo-router/src/context/mod.rs @@ -25,9 +25,9 @@ use crate::services::layers::query_analysis::ParsedDocument; pub(crate) mod extensions; /// The key of the resolved operation name. This is subject to change and should not be relied on. -pub(crate) const OPERATION_NAME: &str = "operation_name"; +pub(crate) const OPERATION_NAME: &str = "apollo::supergraph::operation_name"; /// The key of the resolved operation kind. This is subject to change and should not be relied on. -pub(crate) const OPERATION_KIND: &str = "operation_kind"; +pub(crate) const OPERATION_KIND: &str = "apollo::supergraph::operation_kind"; /// The key to know if the response body contains at least 1 GraphQL error pub(crate) const CONTAINS_GRAPHQL_ERROR: &str = "apollo::telemetry::contains_graphql_error"; diff --git a/apollo-router/src/plugins/authentication/mod.rs b/apollo-router/src/plugins/authentication/mod.rs index 18e906c1ba2..2755c7aa71c 100644 --- a/apollo-router/src/plugins/authentication/mod.rs +++ b/apollo-router/src/plugins/authentication/mod.rs @@ -67,7 +67,7 @@ pub(crate) mod subgraph; mod tests; pub(crate) const AUTHENTICATION_SPAN_NAME: &str = "authentication_plugin"; -pub(crate) const APOLLO_AUTHENTICATION_JWT_CLAIMS: &str = "apollo_authentication::JWT::claims"; +pub(crate) const APOLLO_AUTHENTICATION_JWT_CLAIMS: &str = "apollo::authentication::jwt_claims"; const HEADER_TOKEN_TRUNCATED: &str = "(truncated)"; #[derive(Debug, Display, Error)] diff --git a/apollo-router/src/plugins/authorization/authenticated.rs b/apollo-router/src/plugins/authorization/authenticated.rs index 80ec5805e7a..90dd05c6e1c 100644 --- a/apollo-router/src/plugins/authorization/authenticated.rs +++ b/apollo-router/src/plugins/authorization/authenticated.rs @@ -534,6 +534,7 @@ mod tests { use crate::json_ext::Path; use crate::plugin::test::MockSubgraph; use crate::plugins::authorization::authenticated::AuthenticatedVisitor; + use crate::plugins::authorization::APOLLO_AUTHENTICATION_JWT_CLAIMS; use crate::services::router::ClientRequestAccepts; use crate::services::supergraph; use crate::spec::query::transform; @@ -1502,10 +1503,7 @@ mod tests { let context = Context::new(); context - .insert( - "apollo_authentication::JWT::claims", - "placeholder".to_string(), - ) + .insert(APOLLO_AUTHENTICATION_JWT_CLAIMS, "placeholder".to_string()) .unwrap(); let request = supergraph::Request::fake_builder() .query("query { orga(id: 1) { id creatorUser { id name phone } } }") @@ -1584,7 +1582,7 @@ mod tests { let context = Context::new(); /*context .insert( - "apollo_authentication::JWT::claims", + APOLLO_AUTHENTICATION_JWT_CLAIMS, "placeholder".to_string(), ) .unwrap();*/ @@ -1659,7 +1657,7 @@ mod tests { let context = Context::new(); /*context .insert( - "apollo_authentication::JWT::claims", + APOLLO_AUTHENTICATION_JWT_CLAIMS, "placeholder".to_string(), ) .unwrap();*/ diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index 63b2062a053..98d065c7ee4 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -52,9 +52,10 @@ pub(crate) mod authenticated; pub(crate) mod policy; pub(crate) mod scopes; -const AUTHENTICATED_KEY: &str = "apollo_authorization::authenticated::required"; -const REQUIRED_SCOPES_KEY: &str = "apollo_authorization::scopes::required"; -const REQUIRED_POLICIES_KEY: &str = "apollo_authorization::policies::required"; +pub(crate) const AUTHENTICATION_REQUIRED_KEY: &str = + "apollo::authorization::authentication_required"; +pub(crate) const REQUIRED_SCOPES_KEY: &str = "apollo::authorization::required_scopes"; +pub(crate) const REQUIRED_POLICIES_KEY: &str = "apollo::authorization::required_policies"; #[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)] pub(crate) struct CacheKeyMetadata { @@ -191,7 +192,7 @@ impl AuthorizationPlugin { false, ); if is_authenticated { - context.insert(AUTHENTICATED_KEY, true).unwrap(); + context.insert(AUTHENTICATION_REQUIRED_KEY, true).unwrap(); } if !scopes.is_empty() { @@ -586,7 +587,7 @@ impl Plugin for AuthorizationPlugin { ServiceBuilder::new() .map_request(|request: execution::Request| { let filtered = !request.query_plan.query.unauthorized.paths.is_empty(); - let needs_authenticated = request.context.contains_key(AUTHENTICATED_KEY); + let needs_authenticated = request.context.contains_key(AUTHENTICATION_REQUIRED_KEY); let needs_requires_scopes = request.context.contains_key(REQUIRED_SCOPES_KEY); if needs_authenticated || needs_requires_scopes { diff --git a/apollo-router/src/plugins/authorization/tests.rs b/apollo-router/src/plugins/authorization/tests.rs index be9cb515780..d7320dbec04 100644 --- a/apollo-router/src/plugins/authorization/tests.rs +++ b/apollo-router/src/plugins/authorization/tests.rs @@ -8,6 +8,7 @@ use crate::graphql; use crate::plugin::test::MockSubgraph; use crate::plugin::test::MockSubgraphService; use crate::plugins::authorization::CacheKeyMetadata; +use crate::plugins::authorization::APOLLO_AUTHENTICATION_JWT_CLAIMS; use crate::services::router; use crate::services::router::body; use crate::services::subgraph; @@ -64,10 +65,7 @@ async fn authenticated_request() { let context = Context::new(); context - .insert( - "apollo_authentication::JWT::claims", - "placeholder".to_string(), - ) + .insert(APOLLO_AUTHENTICATION_JWT_CLAIMS, "placeholder".to_string()) .unwrap(); let request = supergraph::Request::fake_builder() .query("query { orga(id: 1) { id creatorUser { id name phone } } }") @@ -304,7 +302,7 @@ async fn authenticated_directive() { let context = Context::new(); context .insert( - "apollo_authentication::JWT::claims", + APOLLO_AUTHENTICATION_JWT_CLAIMS, json! {{ "scope": "user:read" }}, ) .unwrap(); @@ -648,7 +646,7 @@ async fn scopes_directive() { let context = Context::new(); context .insert( - "apollo_authentication::JWT::claims", + APOLLO_AUTHENTICATION_JWT_CLAIMS, json! {{ "scope": "user:read" }}, ) .unwrap(); @@ -679,7 +677,7 @@ async fn scopes_directive() { let context = Context::new(); context .insert( - "apollo_authentication::JWT::claims", + APOLLO_AUTHENTICATION_JWT_CLAIMS, json! {{ "scope": "user:read pii" }}, ) .unwrap(); @@ -710,7 +708,7 @@ async fn scopes_directive() { let context = Context::new(); context .insert( - "apollo_authentication::JWT::claims", + APOLLO_AUTHENTICATION_JWT_CLAIMS, json! {{ "scope": "admin" }}, ) .unwrap(); @@ -1099,7 +1097,7 @@ async fn cache_key_metadata() { let context = Context::new(); context .insert( - "apollo_authentication::JWT::claims", + APOLLO_AUTHENTICATION_JWT_CLAIMS, json! {{ "scope": "id test" }}, ) .unwrap(); diff --git a/apollo-router/src/plugins/demand_control/mod.rs b/apollo-router/src/plugins/demand_control/mod.rs index f2348f31a9f..07c5c65ff58 100644 --- a/apollo-router/src/plugins/demand_control/mod.rs +++ b/apollo-router/src/plugins/demand_control/mod.rs @@ -43,11 +43,10 @@ use crate::Context; pub(crate) mod cost_calculator; pub(crate) mod strategy; -pub(crate) static COST_ESTIMATED_KEY: &str = "cost.estimated"; -pub(crate) static COST_ACTUAL_KEY: &str = "cost.actual"; -pub(crate) static COST_DELTA_KEY: &str = "cost.delta"; -pub(crate) static COST_RESULT_KEY: &str = "cost.result"; -pub(crate) static COST_STRATEGY_KEY: &str = "cost.strategy"; +pub(crate) static COST_ESTIMATED_KEY: &str = "apollo::demand_control::estimated_cost"; +pub(crate) static COST_ACTUAL_KEY: &str = "apollo::demand_control::actual_cost"; +pub(crate) static COST_RESULT_KEY: &str = "apollo::demand_control::result"; +pub(crate) static COST_STRATEGY_KEY: &str = "apollo::demand_control::strategy"; /// Algorithm for calculating the cost of an incoming query. #[derive(Clone, Debug, Deserialize, JsonSchema)] diff --git a/apollo-router/src/plugins/expose_query_plan.rs b/apollo-router/src/plugins/expose_query_plan.rs index f83a1d933b2..a5d80db5534 100644 --- a/apollo-router/src/plugins/expose_query_plan.rs +++ b/apollo-router/src/plugins/expose_query_plan.rs @@ -20,9 +20,9 @@ use crate::services::supergraph; const EXPOSE_QUERY_PLAN_HEADER_NAME: &str = "Apollo-Expose-Query-Plan"; const ENABLE_EXPOSE_QUERY_PLAN_ENV: &str = "APOLLO_EXPOSE_QUERY_PLAN"; -const QUERY_PLAN_CONTEXT_KEY: &str = "experimental::expose_query_plan.plan"; -const FORMATTED_QUERY_PLAN_CONTEXT_KEY: &str = "experimental::expose_query_plan.formatted_plan"; -const ENABLED_CONTEXT_KEY: &str = "experimental::expose_query_plan.enabled"; +const QUERY_PLAN_CONTEXT_KEY: &str = "apollo::expose_query_plan::plan"; +const FORMATTED_QUERY_PLAN_CONTEXT_KEY: &str = "apollo::expose_query_plan::formatted_plan"; +const ENABLED_CONTEXT_KEY: &str = "apollo::expose_query_plan::enabled"; #[derive(Debug, Clone)] struct ExposeQueryPlan { diff --git a/apollo-router/src/plugins/progressive_override/mod.rs b/apollo-router/src/plugins/progressive_override/mod.rs index d4e1adb9b53..32d4a1b472f 100644 --- a/apollo-router/src/plugins/progressive_override/mod.rs +++ b/apollo-router/src/plugins/progressive_override/mod.rs @@ -24,8 +24,8 @@ use crate::spec; use crate::spec::query::traverse; pub(crate) mod visitor; -pub(crate) const UNRESOLVED_LABELS_KEY: &str = "apollo_override::unresolved_labels"; -pub(crate) const LABELS_TO_OVERRIDE_KEY: &str = "apollo_override::labels_to_override"; +pub(crate) const UNRESOLVED_LABELS_KEY: &str = "apollo::progressive_override::unresolved_labels"; +pub(crate) const LABELS_TO_OVERRIDE_KEY: &str = "apollo::progressive_override::labels_to_override"; pub(crate) const JOIN_FIELD_DIRECTIVE_NAME: &str = "join__field"; pub(crate) const JOIN_SPEC_BASE_URL: &str = "https://specs.apollo.dev/join"; diff --git a/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs b/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs index c79ca7af176..6f198d43bf1 100644 --- a/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs @@ -15,10 +15,6 @@ use super::instruments::Increment; use super::instruments::StaticInstrument; use crate::graphql; use crate::metrics; -use crate::plugins::demand_control::COST_ACTUAL_KEY; -use crate::plugins::demand_control::COST_DELTA_KEY; -use crate::plugins::demand_control::COST_ESTIMATED_KEY; -use crate::plugins::demand_control::COST_RESULT_KEY; use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config_new::attributes::SupergraphAttributes; use crate::plugins::telemetry::config_new::conditions::Condition; @@ -43,6 +39,11 @@ pub(crate) const APOLLO_PRIVATE_COST_STRATEGY: Key = pub(crate) const APOLLO_PRIVATE_COST_RESULT: Key = Key::from_static_str("apollo_private.cost.result"); +const COST_ACTUAL_KEY: &str = "cost.actual"; +const COST_DELTA_KEY: &str = "cost.delta"; +const COST_ESTIMATED_KEY: &str = "cost.estimated"; +const COST_RESULT_KEY: &str = "cost.result"; + /// Attributes for Cost #[derive(Deserialize, JsonSchema, Clone, Default, Debug, PartialEq)] #[serde(deny_unknown_fields, default)] @@ -121,7 +122,7 @@ impl SupergraphCostAttributes { let key = self .cost_delta .as_ref()? - .key(Key::from_static_str("cost.delta"))?; + .key(Key::from_static_str(COST_DELTA_KEY))?; let value = ctx.get_cost_delta().ok()??; Some(KeyValue::new(key, value)) } diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_on_error/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_on_error/router.yaml index 3b739ec2eef..7065393551b 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_on_error/router.yaml +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_on_error/router.yaml @@ -11,7 +11,7 @@ telemetry: unit: request attributes: graphql.operation.name: - response_context: operation_name + response_context: "apollo::supergraph::operation_name" condition: eq: - "request timed out" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_on_error/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_on_error/test.yaml index b3b61fd94e9..483311b4a95 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_on_error/test.yaml +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_on_error/test.yaml @@ -2,7 +2,7 @@ description: Custom counter should be incremented on timeout error with operatio events: - - context: map: - operation_name: TestQuery + "apollo::supergraph::operation_name": TestQuery - router_request: uri: "/hello" method: POST diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/test.yaml index 65b19646657..ce313c5b4a9 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/test.yaml +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/test.yaml @@ -7,7 +7,7 @@ events: hello - context: map: - operation_name: TestQuery + "apollo::supergraph::operation_name": TestQuery - router_response: body: | hello diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/test.yaml index 369491b02e6..03783d8a643 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/test.yaml +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/test.yaml @@ -9,7 +9,7 @@ events: hello - context: map: - "operation_name": "Test" + "apollo::supergraph::operation_name": "Test" - supergraph_request: uri: "/hello" method: GET diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index d0c4c85878b..5c33c5e2cbb 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -137,6 +137,7 @@ use crate::router_factory::Endpoint; use crate::services::connector_service::CONNECTOR_INFO_CONTEXT_KEY; use crate::services::execution; use crate::services::http::HttpRequest; +use crate::services::layers::apq::PERSISTED_QUERY_CACHE_HIT; use crate::services::router; use crate::services::subgraph; use crate::services::supergraph; @@ -171,10 +172,10 @@ pub(crate) mod tracing; pub(crate) mod utils; // Tracing consts -pub(crate) const CLIENT_NAME: &str = "apollo_telemetry::client_name"; -const CLIENT_VERSION: &str = "apollo_telemetry::client_version"; -const SUBGRAPH_FTV1: &str = "apollo_telemetry::subgraph_ftv1"; -pub(crate) const STUDIO_EXCLUDE: &str = "apollo_telemetry::studio::exclude"; +pub(crate) const CLIENT_NAME: &str = "apollo::telemetry::client_name"; +const CLIENT_VERSION: &str = "apollo::telemetry::client_version"; +const SUBGRAPH_FTV1: &str = "apollo::telemetry::subgraph_ftv1"; +pub(crate) const STUDIO_EXCLUDE: &str = "apollo::telemetry::studio_exclude"; pub(crate) const SUPERGRAPH_SCHEMA_ID_CONTEXT_KEY: &str = "apollo::supergraph_schema_id"; const GLOBAL_TRACER_NAME: &str = "apollo-router"; const DEFAULT_EXPOSE_TRACE_ID_HEADER: &str = "apollo-trace-id"; @@ -1359,7 +1360,7 @@ impl Telemetry { let licensed_operation_count = licensed_operation_count(&usage_reporting.stats_report_key); let persisted_query_hit = context - .get::<_, bool>("persisted_query_hit") + .get::<_, bool>(PERSISTED_QUERY_CACHE_HIT) .unwrap_or_default(); if context diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index 90e88e04ab1..9be1d5c0843 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -47,7 +47,7 @@ use crate::Configuration; pub(crate) type Plugins = IndexMap>; pub(crate) type InMemoryCachePlanner = InMemoryCache>>; -pub(crate) const APOLLO_OPERATION_ID: &str = "apollo_operation_id"; +pub(crate) const APOLLO_OPERATION_ID: &str = "apollo::supergraph::operation_id"; /// A query planner wrapper that caches results. /// diff --git a/apollo-router/src/services/layers/apq.rs b/apollo-router/src/services/layers/apq.rs index d4f1119161a..cb0c2ba5bed 100644 --- a/apollo-router/src/services/layers/apq.rs +++ b/apollo-router/src/services/layers/apq.rs @@ -18,6 +18,8 @@ use crate::services::SupergraphResponse; const DONT_CACHE_RESPONSE_VALUE: &str = "private, no-cache, must-revalidate"; static DONT_CACHE_HEADER_VALUE: HeaderValue = HeaderValue::from_static(DONT_CACHE_RESPONSE_VALUE); +pub(crate) const PERSISTED_QUERY_CACHE_HIT: &str = "apollo::apq::cache_hit"; +pub(crate) const PERSISTED_QUERY_REGISTERED: &str = "apollo::apq::registered"; /// A persisted query. #[derive(Deserialize, Clone, Debug)] @@ -95,7 +97,7 @@ async fn apq_request( (Some((query_hash, query_hash_bytes)), Some(query)) => { if query_matches_hash(query.as_str(), query_hash_bytes.as_slice()) { tracing::trace!("apq: cache insert"); - let _ = request.context.insert("persisted_query_register", true); + let _ = request.context.insert(PERSISTED_QUERY_REGISTERED, true); let query = query.to_owned(); let cache = cache.clone(); tokio::spawn(async move { @@ -130,12 +132,12 @@ async fn apq_request( .get() .await { - let _ = request.context.insert("persisted_query_hit", true); + let _ = request.context.insert(PERSISTED_QUERY_CACHE_HIT, true); tracing::trace!("apq: cache hit"); request.supergraph_request.body_mut().query = Some(cached_query); Ok(request) } else { - let _ = request.context.insert("persisted_query_hit", false); + let _ = request.context.insert(PERSISTED_QUERY_CACHE_HIT, false); tracing::trace!("apq: cache miss"); let errors = vec![crate::error::Error { message: "PersistedQueryNotFound".to_string(), diff --git a/apollo-router/src/services/supergraph/service.rs b/apollo-router/src/services/supergraph/service.rs index 2c01813446f..672a5590c6f 100644 --- a/apollo-router/src/services/supergraph/service.rs +++ b/apollo-router/src/services/supergraph/service.rs @@ -82,7 +82,7 @@ use crate::Configuration; use crate::Context; use crate::Notify; -pub(crate) const FIRST_EVENT_CONTEXT_KEY: &str = "apollo_router::supergraph::first_event"; +pub(crate) const FIRST_EVENT_CONTEXT_KEY: &str = "apollo::supergraph::first_event"; /// An [`IndexMap`] of available plugins. pub(crate) type Plugins = IndexMap>; diff --git a/apollo-router/tests/integration/coprocessor.rs b/apollo-router/tests/integration/coprocessor.rs index 791eb2141a1..604728df2db 100644 --- a/apollo-router/tests/integration/coprocessor.rs +++ b/apollo-router/tests/integration/coprocessor.rs @@ -223,9 +223,9 @@ async fn test_coprocessor_demand_control_access() -> Result<(), BoxError> { "stage": "ExecutionRequest", "context": { "entries": { - "cost.estimated": 10.0, - "cost.result": "COST_OK", - "cost.strategy": "static_estimated" + "apollo::demand_control::estimated_cost": 10.0, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::strategy": "static_estimated" }}}))) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "version":1, @@ -242,10 +242,10 @@ async fn test_coprocessor_demand_control_access() -> Result<(), BoxError> { .and(body_partial_json(json!({ "stage": "SupergraphResponse", "context": {"entries": { - "cost.actual": 3.0, - "cost.estimated": 10.0, - "cost.result": "COST_OK", - "cost.strategy": "static_estimated" + "apollo::demand_control::actual_cost": 3.0, + "apollo::demand_control::estimated_cost": 10.0, + "apollo::demand_control::result": "COST_OK", + "apollo::demand_control::strategy": "static_estimated" }}}))) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "version":1, diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index 07d16d7b925..7b72dd3410b 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -778,7 +778,7 @@ async fn entity_cache_authorization() -> Result<(), BoxError> { let context = Context::new(); context .insert( - "apollo_authorization::scopes::required", + "apollo::authorization::required_scopes", json! {["profile", "read:user", "read:name"]}, ) .unwrap(); @@ -837,13 +837,13 @@ async fn entity_cache_authorization() -> Result<(), BoxError> { let context = Context::new(); context .insert( - "apollo_authorization::scopes::required", + "apollo::authorization::required_scopes", json! {["profile", "read:user", "read:name"]}, ) .unwrap(); context .insert( - "apollo_authentication::JWT::claims", + "apollo::authentication::jwt_claims", json! {{ "scope": "read:user read:name" }}, ) .unwrap(); @@ -882,13 +882,13 @@ async fn entity_cache_authorization() -> Result<(), BoxError> { let context = Context::new(); context .insert( - "apollo_authorization::scopes::required", + "apollo::authorization::required_scopes", json! {["profile", "read:user", "read:name"]}, ) .unwrap(); context .insert( - "apollo_authentication::JWT::claims", + "apollo::authentication::jwt_claims", json! {{ "scope": "read:user profile" }}, ) .unwrap(); diff --git a/apollo-router/tests/integration_tests.rs b/apollo-router/tests/integration_tests.rs index 30dd22efe5c..db82b60700f 100644 --- a/apollo-router/tests/integration_tests.rs +++ b/apollo-router/tests/integration_tests.rs @@ -1039,7 +1039,7 @@ async fn query_operation_id() { expected_apollo_operation_id, response .context - .get::<_, String>("apollo_operation_id".to_string()) + .get::<_, String>("apollo::supergraph::operation_id") .unwrap() .unwrap() .as_str() @@ -1065,7 +1065,7 @@ async fn query_operation_id() { expected_apollo_operation_id, response .context - .get::<_, String>("apollo_operation_id".to_string()) + .get::<_, String>("apollo::supergraph::operation_id") .unwrap() .unwrap() .as_str() @@ -1085,7 +1085,7 @@ async fn query_operation_id() { // "## GraphQLParseFailure\n" response .context - .get::<_, String>("apollo_operation_id".to_string()) + .get::<_, String>("apollo::supergraph::operation_id") .unwrap() .is_none() ); @@ -1109,7 +1109,7 @@ async fn query_operation_id() { // "## GraphQLUnknownOperationName\n" assert!(response .context - .get::<_, String>("apollo_operation_id".to_string()) + .get::<_, String>("apollo::supergraph::operation_id") .unwrap() .is_none()); @@ -1132,7 +1132,7 @@ async fn query_operation_id() { // "## GraphQLValidationFailure\n" assert!(response .context - .get::<_, String>("apollo_operation_id".to_string()) + .get::<_, String>("apollo::supergraph::operation_id") .unwrap() .is_none()); } diff --git a/apollo-router/tests/samples/enterprise/progressive-override/basic/rhai/main.rhai b/apollo-router/tests/samples/enterprise/progressive-override/basic/rhai/main.rhai index 3ecb55cca97..81891750e62 100644 --- a/apollo-router/tests/samples/enterprise/progressive-override/basic/rhai/main.rhai +++ b/apollo-router/tests/samples/enterprise/progressive-override/basic/rhai/main.rhai @@ -6,16 +6,16 @@ fn supergraph_service(service) { // Add a timestamp to context which we'll use in the response. fn process_request(request) { request.context["request_start"] = Router.APOLLO_START.elapsed; - let labels = request.context["apollo_override::unresolved_labels"]; + let labels = request.context["apollo::progressive_override::unresolved_labels"]; print(`unresolved: ${labels}`); - let override = request.context["apollo_override::labels_to_override"]; + let override = request.context["apollo::progressive_override::labels_to_override"]; print(`override: ${override}`); if "x-override" in request.headers { if request.headers["x-override"] == "true" { - request.context["apollo_override::labels_to_override"] += "bar"; + request.context["apollo::progressive_override::labels_to_override"] += "bar"; } } } diff --git a/apollo-router/tests/samples/enterprise/progressive-override/warmup/rhai/main.rhai b/apollo-router/tests/samples/enterprise/progressive-override/warmup/rhai/main.rhai index 3ecb55cca97..81891750e62 100644 --- a/apollo-router/tests/samples/enterprise/progressive-override/warmup/rhai/main.rhai +++ b/apollo-router/tests/samples/enterprise/progressive-override/warmup/rhai/main.rhai @@ -6,16 +6,16 @@ fn supergraph_service(service) { // Add a timestamp to context which we'll use in the response. fn process_request(request) { request.context["request_start"] = Router.APOLLO_START.elapsed; - let labels = request.context["apollo_override::unresolved_labels"]; + let labels = request.context["apollo::progressive_override::unresolved_labels"]; print(`unresolved: ${labels}`); - let override = request.context["apollo_override::labels_to_override"]; + let override = request.context["apollo::progressive_override::labels_to_override"]; print(`override: ${override}`); if "x-override" in request.headers { if request.headers["x-override"] == "true" { - request.context["apollo_override::labels_to_override"] += "bar"; + request.context["apollo::progressive_override::labels_to_override"] += "bar"; } } } diff --git a/docs/source/reference/migration/from-router-v1.mdx b/docs/source/reference/migration/from-router-v1.mdx index ae0d3bfd605..879bd745fb2 100644 --- a/docs/source/reference/migration/from-router-v1.mdx +++ b/docs/source/reference/migration/from-router-v1.mdx @@ -176,6 +176,34 @@ telemetry: - `apollo_router_session_count_total` has been removed and replaced by `http.server.active_requests`. +### Context Keys + +The router request context is used to share data across stages of the request pipeline. The keys have been renamed for consistency and to better indicate which pipeline stage or plugin populates the data. If you access context entries in a custom plugin, Rhai script, coprocessor, or telemetry selector, update your context keys to account for the new names: + +- `apollo_authentication::JWT::claims` -> `apollo::authentication::jwt_claims` +- `apollo_authorization::authenticated::required` -> `apollo::authorization::authentication_required` +- `apollo_authorization::scopes::required` -> `apollo::authorization::required_scopes` +- `apollo_authorization::policies::required` -> `apollo::authorization::required_policies` +- `apollo_operation_id` -> `apollo::supergraph::operation_id` +- `apollo_override::unresolved_labels` -> `apollo::progressive_override::unresolved_labels` +- `apollo_override::labels_to_override` -> `apollo::progressive_override::labels_to_override` +- `apollo_router::supergraph::first_event` -> `apollo::supergraph::first_event` +- `apollo_telemetry::client_name` -> `apollo::telemetry::client_name` +- `apollo_telemetry::client_version` -> `apollo::telemetry::client_version` +- `apollo_telemetry::studio::exclude` -> `apollo::telemetry::studio_exclude` +- `apollo_telemetry::subgraph_ftv1` -> `apollo::telemetry::subgraph_ftv1` +- `cost.actual` -> `apollo::demand_control::actual_cost` +- `cost.estimated` -> `apollo::demand_control::estimated_cost` +- `cost.result` -> `apollo::demand_control::result` +- `cost.strategy` -> `apollo::demand_control::strategy` +- `experimental::expose_query_plan.enabled` -> `apollo::expose_query_plan::enabled` +- `experimental::expose_query_plan.formatted_plan` -> `apollo::expose_query_plan::formatted_plan` +- `experimental::expose_query_plan.plan` -> `apollo::expose_query_plan::plan` +- `operation_kind` -> `apollo::supergraph::operation_kind` +- `operation_name` -> `apollo::supergraph::operation_name` +- `persisted_query_hit` -> `apollo::apq::cache_hit` +- `persisted_query_register` -> `apollo::apq::registered` + ### Tracing `jaeger` exporter has been removed as Jaeger fully supports to OTLP format. To use the `otlp` exporter change your router config: diff --git a/docs/source/routing/customization/coprocessor.mdx b/docs/source/routing/customization/coprocessor.mdx index 314182b008e..a0a269c1c14 100644 --- a/docs/source/routing/customization/coprocessor.mdx +++ b/docs/source/routing/customization/coprocessor.mdx @@ -4,7 +4,7 @@ subtitle: Customize your router's behavior in any language description: Customize the Apollo GraphOS Router with external coprocessing. Write standalone code in any language, hook into request lifecycle, and modify request/response details. --- -import CoprocTypicalConfig from '../../../shared/coproc-typical-config.mdx'; +import CoprocTypicalConfig from "../../../shared/coproc-typical-config.mdx"; @@ -55,10 +55,10 @@ As shown in the diagram above, the `RouterService`, `SupergraphService`, `Execut Each supported service can send its coprocessor requests at two different **stages**: - As execution proceeds "down" from the client to individual subgraphs - - Here, the coprocessor can inspect and modify details of requests before GraphQL operations are processed. - - The coprocessor can also instruct the router to [_terminate_ a client request](#terminating-a-client-request) immediately. + - Here, the coprocessor can inspect and modify details of requests before GraphQL operations are processed. + - The coprocessor can also instruct the router to [_terminate_ a client request](#terminating-a-client-request) immediately. - As execution proceeds back "up" from subgraphs to the client - - Here, the coprocessor can inspect and modify details of the router's response to the client. + - Here, the coprocessor can inspect and modify details of the router's response to the client. At _every_ stage, the router waits for your coprocessor's response before it continues processing the corresponding request. Because of this, you should maximize responsiveness by configuring _only_ whichever coprocessor requests your customization requires. @@ -76,7 +76,7 @@ You configure external coprocessing in your router's [YAML config file](/router/ This example configuration sends commonly used request and response details to your coprocessor (see the comments below for explanations of each field): - + ### Minimal configuration @@ -100,39 +100,36 @@ You can define [conditions](/router/configuration/telemetry/instrumentation/cond The `Execution` stage doesn't support coprocessor conditions. - - + Example configurations: - - Run during the `SupergraphResponse` stage only for the first event of a supergraph response. Useful for handling only the first subscription event when a subscription is opened: - +- Run during the `SupergraphResponse` stage only for the first event of a supergraph response. Useful for handling only the first subscription event when a subscription is opened: ```yaml title="router.yaml" coprocessor: url: http://127.0.0.1:3000 supergraph: - response: + response: condition: eq: - - true - - is_primary_response: true # Will be true only for the first event received on a supergraph response (like classical queries and mutations for example) + - true + - is_primary_response: true # Will be true only for the first event received on a supergraph response (like classical queries and mutations for example) body: true headers: true ``` - Run during the `Request` stage only if the request contains a request header: - ```yaml title="router.yaml" coprocessor: url: http://127.0.0.1:3000 router: - request: + request: condition: eq: - - request_header: should-execute-copro # Header name - - "enabled" # Header value + - request_header: should-execute-copro # Header name + - "enabled" # Header value body: true headers: true ``` @@ -149,7 +146,6 @@ coprocessor: # Using an HTTP (not HTTPS) URL and experimental_http2: http2only results in connections that use h2c client: experimental_http2: http2only - ``` ## Coprocessor request format @@ -165,11 +161,11 @@ The router communicates with your coprocessor via HTTP POST requests (called **c Properties of the JSON body are divided into two high-level categories: - "Control" properties - - These provide information about the context of the specific router request or response. They provide a mechanism to influence the router's execution flow. - - The router always includes these properties in coprocessor requests. + - These provide information about the context of the specific router request or response. They provide a mechanism to influence the router's execution flow. + - The router always includes these properties in coprocessor requests. - Data properties - - These provide information about the substance of a request or response, such as the GraphQL query string and any HTTP headers. Aside from `sdl`, your coprocessor can modify all of these properties. - - You [configure which of these fields](#setup) the router includes in its coprocessor requests. By default, the router includes _none_ of them. + - These provide information about the substance of a request or response, such as the GraphQL query string and any HTTP headers. Aside from `sdl`, your coprocessor can modify all of these properties. + - You [configure which of these fields](#setup) the router includes in its coprocessor requests. By default, the router includes _none_ of them. ### Example requests by stage @@ -263,7 +259,7 @@ Properties of the JSON body are divided into two high-level categories: "apollo_telemetry::subgraph_metrics_attributes": {}, "accepts-json": false, "accepts-multipart": false, - "apollo_telemetry::client_name": "manual", + "apollo::telemetry::client_name": "manual", "apollo_telemetry::usage_reporting": { "statsReportKey": "# Long\nquery Long{me{name}}", "referencedFieldsByType": { @@ -281,7 +277,7 @@ Properties of the JSON body are divided into two high-level categories: } } }, - "apollo_telemetry::client_version": "", + "apollo::telemetry::client_version": "", "accepts-wildcard": true } }, @@ -297,7 +293,6 @@ Properties of the JSON body are divided into two high-level categories: ```json - { // Control properties "version": 1, @@ -306,30 +301,14 @@ Properties of the JSON body are divided into two high-level categories: // Data properties "headers": { - "cookie": [ - "tasty_cookie=strawberry" - ], - "content-type": [ - "application/json" - ], - "host": [ - "127.0.0.1:4000" - ], - "apollo-federation-include-trace": [ - "ftv1" - ], - "apollographql-client-name": [ - "manual" - ], - "accept": [ - "*/*" - ], - "user-agent": [ - "curl/7.79.1" - ], - "content-length": [ - "46" - ] + "cookie": ["tasty_cookie=strawberry"], + "content-type": ["application/json"], + "host": ["127.0.0.1:4000"], + "apollo-federation-include-trace": ["ftv1"], + "apollographql-client-name": ["manual"], + "accept": ["*/*"], + "user-agent": ["curl/7.79.1"], + "content-length": ["46"] }, "body": { "query": "query Long {\n me {\n name\n}\n}", @@ -347,7 +326,6 @@ Properties of the JSON body are divided into two high-level categories: "serviceName": "service name shouldn't change", "uri": "http://thisurihaschanged" } - ``` @@ -357,7 +335,6 @@ Properties of the JSON body are divided into two high-level categories: ```json - { // Control properties "version": 1, @@ -379,7 +356,6 @@ Properties of the JSON body are divided into two high-level categories: "aheader": ["a value"] } } - ``` @@ -389,7 +365,6 @@ Properties of the JSON body are divided into two high-level categories: ```json - { // Control properties "version": 1, @@ -398,30 +373,14 @@ Properties of the JSON body are divided into two high-level categories: // Data properties "headers": { - "cookie": [ - "tasty_cookie=strawberry" - ], - "content-type": [ - "application/json" - ], - "host": [ - "127.0.0.1:4000" - ], - "apollo-federation-include-trace": [ - "ftv1" - ], - "apollographql-client-name": [ - "manual" - ], - "accept": [ - "*/*" - ], - "user-agent": [ - "curl/7.79.1" - ], - "content-length": [ - "46" - ] + "cookie": ["tasty_cookie=strawberry"], + "content-type": ["application/json"], + "host": ["127.0.0.1:4000"], + "apollo-federation-include-trace": ["ftv1"], + "apollographql-client-name": ["manual"], + "accept": ["*/*"], + "user-agent": ["curl/7.79.1"], + "content-length": ["46"] }, "body": { "query": "query Long {\n me {\n name\n}\n}", @@ -438,24 +397,86 @@ Properties of the JSON body are divided into two high-level categories: "serviceName": "service name shouldn't change", "uri": "http://thisurihaschanged", "queryPlan": { - "usage_reporting":{"statsReportKey":"# Me\nquery Me{me{name username}}","referencedFieldsByType":{"User":{"fieldNames":["name","username"],"isInterface":false},"Query":{"fieldNames":["me"],"isInterface":false}}}, - "root":{ - "kind":"Fetch", - "serviceName":"accounts", - "variableUsages":[], - "operation":"query Me__accounts__0{me{name username}}", - "operationName":"Me__accounts__0", - "operationKind":"query", - "id":null, - "inputRewrites":null, - "outputRewrites":null, - "authorization":{"is_authenticated":false,"scopes":[],"policies":[]}}, - "formatted_query_plan":"QueryPlan {\n Fetch(service: \"accounts\") {\n {\n me {\n name\n username\n }\n }\n },\n}", - "query":{ - "string":"query Me {\n me {\n name\n username\n }\n}\n","fragments":{"map":{}},"operations":[{"name":"Me","kind":"query","type_name":"Query","selection_set":[{"Field":{"name":"me","alias":null,"selection_set":[{"Field":{"name":"name","alias":null,"selection_set":null,"field_type":{"Named":"String"},"include_skip":{"include":"Yes","skip":"No"}}},{"Field":{"name":"username","alias":null,"selection_set":null,"field_type":{"Named":"String"},"include_skip":{"include":"Yes","skip":"No"}}}],"field_type":{"Named":"User"},"include_skip":{"include":"Yes","skip":"No"}}}],"variables":{}}],"subselections":{},"unauthorized":{"paths":[],"errors":{"log":true,"response":"errors"}},"filtered_query":null,"defer_stats":{"has_defer":false,"has_unconditional_defer":false,"conditional_defer_variable_names":[]},"is_original":true} + "usage_reporting": { + "statsReportKey": "# Me\nquery Me{me{name username}}", + "referencedFieldsByType": { + "User": { "fieldNames": ["name", "username"], "isInterface": false }, + "Query": { "fieldNames": ["me"], "isInterface": false } + } + }, + "root": { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "query Me__accounts__0{me{name username}}", + "operationName": "Me__accounts__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] } + }, + "formatted_query_plan": "QueryPlan {\n Fetch(service: \"accounts\") {\n {\n me {\n name\n username\n }\n }\n },\n}", + "query": { + "string": "query Me {\n me {\n name\n username\n }\n}\n", + "fragments": { "map": {} }, + "operations": [ + { + "name": "Me", + "kind": "query", + "type_name": "Query", + "selection_set": [ + { + "Field": { + "name": "me", + "alias": null, + "selection_set": [ + { + "Field": { + "name": "name", + "alias": null, + "selection_set": null, + "field_type": { "Named": "String" }, + "include_skip": { "include": "Yes", "skip": "No" } + } + }, + { + "Field": { + "name": "username", + "alias": null, + "selection_set": null, + "field_type": { "Named": "String" }, + "include_skip": { "include": "Yes", "skip": "No" } + } + } + ], + "field_type": { "Named": "User" }, + "include_skip": { "include": "Yes", "skip": "No" } + } + } + ], + "variables": {} + } + ], + "subselections": {}, + "unauthorized": { + "paths": [], + "errors": { "log": true, "response": "errors" } + }, + "filtered_query": null, + "defer_stats": { + "has_defer": false, + "has_unconditional_defer": false, + "conditional_defer_variable_names": [] + }, + "is_original": true + } + } } - ``` @@ -465,7 +486,6 @@ Properties of the JSON body are divided into two high-level categories: ```json - { // Control properties "version": 1, @@ -487,7 +507,6 @@ Properties of the JSON body are divided into two high-level categories: "aheader": ["a value"] } } - ``` @@ -497,7 +516,6 @@ Properties of the JSON body are divided into two high-level categories: ```json - { // Control properties "version": 1, @@ -534,38 +552,28 @@ Properties of the JSON body are divided into two high-level categories: "statsReportKey": "# TopProducts\nquery TopProducts{topProducts{name price reviews{body id}}}", "referencedFieldsByType": { "Query": { - "fieldNames": [ - "topProducts" - ], + "fieldNames": ["topProducts"], "isInterface": false }, "Review": { - "fieldNames": [ - "body", - "id" - ], + "fieldNames": ["body", "id"], "isInterface": false }, "Product": { - "fieldNames": [ - "price", - "name", - "reviews" - ], + "fieldNames": ["price", "name", "reviews"], "isInterface": false } } }, - "apollo_telemetry::client_version": "", + "apollo::telemetry::client_version": "", "apollo_telemetry::subgraph_metrics_attributes": {}, - "apollo_telemetry::client_name": "" + "apollo::telemetry::client_name": "" } }, "uri": "https://reviews.demo.starstuff.dev/", "method": "POST", "serviceName": "reviews" } - ``` @@ -575,7 +583,6 @@ Properties of the JSON body are divided into two high-level categories: ```json - { // Control properties "version": 1, @@ -586,39 +593,17 @@ Properties of the JSON body are divided into two high-level categories: // Data properties "headers": { - "etag": [ - "W/\"d3-7aayASjs0+e2c/TpiAYgEu/yyo0\"" - ], - "via": [ - "2 fly.io" - ], - "server": [ - "Fly/90d459b3 (2023-03-07)" - ], - "date": [ - "Thu, 09 Mar 2023 14:28:46 GMT" - ], - "x-powered-by": [ - "Express" - ], - "x-ratelimit-limit": [ - "10000000" - ], - "access-control-allow-origin": [ - "*" - ], - "x-ratelimit-remaining": [ - "9999478" - ], - "content-type": [ - "application/json; charset=utf-8" - ], - "fly-request-id": [ - "01GV3CCG5EM3ZNVZD2GH0B00E2-lhr" - ], - "x-ratelimit-reset": [ - "1678374007" - ] + "etag": ["W/\"d3-7aayASjs0+e2c/TpiAYgEu/yyo0\""], + "via": ["2 fly.io"], + "server": ["Fly/90d459b3 (2023-03-07)"], + "date": ["Thu, 09 Mar 2023 14:28:46 GMT"], + "x-powered-by": ["Express"], + "x-ratelimit-limit": ["10000000"], + "access-control-allow-origin": ["*"], + "x-ratelimit-remaining": ["9999478"], + "content-type": ["application/json; charset=utf-8"], + "fly-request-id": ["01GV3CCG5EM3ZNVZD2GH0B00E2-lhr"], + "x-ratelimit-reset": ["1678374007"] }, "body": { "data": { @@ -660,37 +645,27 @@ Properties of the JSON body are divided into two high-level categories: "statsReportKey": "# TopProducts\nquery TopProducts{topProducts{name price reviews{body id}}}", "referencedFieldsByType": { "Product": { - "fieldNames": [ - "price", - "name", - "reviews" - ], + "fieldNames": ["price", "name", "reviews"], "isInterface": false }, "Query": { - "fieldNames": [ - "topProducts" - ], + "fieldNames": ["topProducts"], "isInterface": false }, "Review": { - "fieldNames": [ - "body", - "id" - ], + "fieldNames": ["body", "id"], "isInterface": false } } }, - "apollo_telemetry::client_version": "", + "apollo::telemetry::client_version": "", "apollo_telemetry::subgraph_metrics_attributes": {}, - "apollo_telemetry::client_name": "" + "apollo::telemetry::client_name": "" } }, "serviceName": "reviews", "statusCode": 200 } - ``` @@ -796,6 +771,7 @@ This value is one of the following: - `SubgraphResponse`: The `SubgraphService` has just received a subgraph response. **Do not return a _different_ value for this property.** If you do, the router treats the coprocessor request as if it failed. + @@ -905,7 +881,7 @@ When `stage` is `SupergraphResponse`, if present and `true` then there will be s An object mapping of all HTTP header names and values for the corresponding request or response. -Ensure headers are handled like HTTP headers in general. For example, normalize header case before your coprocessor operates on them. +Ensure headers are handled like HTTP headers in general. For example, normalize header case before your coprocessor operates on them. If your coprocessor [returns a _different_ value](#responding-to-coprocessor-requests) for `headers`, the router replaces the existing headers with that value. @@ -960,7 +936,6 @@ This value can be very large, so you should avoid including it in coprocessor re The router ignores modifications to this value. - @@ -1031,7 +1006,6 @@ When `stage` is `ExecutionRequest`, this contains the query plan for the client - ## Responding to coprocessor requests The router expects your coprocessor to respond with a `200` status code and a JSON body that matches the structure of the [request body](#example-requests-by-stage). @@ -1041,7 +1015,7 @@ In the response body, your coprocessor can return _modified values_ for certain The router supports modifying the following properties from your coprocessor: - [`control`](#control) - - Modify this property to immediately [terminate a client request](#terminating-a-client-request). + - Modify this property to immediately [terminate a client request](#terminating-a-client-request). - [`body`](#body) - [`headers`](#headers) - [`context`](#context) @@ -1080,8 +1054,8 @@ If the router receives an object with this format for `control`, it immediately - The HTTP status code is set to the value of the `break` property (`401` in the example above). - The response body is the coprocessor's returned value for `body`. - - The value of `body` should adhere to the standard GraphQL JSON response format (see the example above). - - Alternatively, you can specify a string value for `body`. If you do, the router returns an error response with that string as the error's `message`. + - The value of `body` should adhere to the standard GraphQL JSON response format (see the example above). + - Alternatively, you can specify a string value for `body`. If you do, the router returns an error response with that string as the error's `message`. The example response above sets the HTTP status code to `400`, which indicates a failed request. @@ -1145,7 +1119,6 @@ If a request to a coprocessor results in a **failed response**, which is seperat - Your coprocessor's response body doesn't match the JSON structure of the corresponding [request body](#example-requests-by-stage). - Your coprocessor's response body sets different values for [control properties](#property-reference) that must not change, such as `stage` and `version`. - ## Handling deferred query responses GraphOS Router and Apollo Router Core support the incremental delivery of query response data via [the `@defer` directive](/router/executing-operations/defer-support/): @@ -1166,6 +1139,7 @@ For a single query with deferred fields, your router sends multiple "chunks" of - The [`status_code`](#status_code) and [`headers`](#headers) fields are included only in the coprocessor request for any response's _first_ chunk. These values can't change after the first chunk is returned to the client, so they're subsequently omitted. - If your coprocessor modifes the response [`body`](#body) for a response chunk, it must provide the new value as a _string_, _not_ as an object. This is because response chunk bodies include multipart boundary information in addition to the actual serialized JSON response data. [See examples.](#examples-of-deferred-response-chunks) + - Many responses will not contain deferred streams and for these the body string can usually be fairly reliably transformed into a JSON object for easy manipulation within the coprocessor. Coprocessors should be carefully coded to allow for the presence of a body that is not a valid JSON object. - Because the data is a JSON string at both `RouterRequest` and `RouterResponse`, it's entirely possible for a coprocessor to rewrite the body from invalid JSON content into valid JSON content. This is one of the primary use cases for `RouterRequest` body processing. @@ -1186,20 +1160,16 @@ The first response chunk includes `headers` and `statusCode` fields: "sdl": "...", // String omitted due to length // highlight-start "headers": { - "content-type": [ - "multipart/mixed;boundary=\"graphql\";deferSpec=20220824" - ], - "vary": [ - "origin" - ] + "content-type": ["multipart/mixed;boundary=\"graphql\";deferSpec=20220824"], + "vary": ["origin"] }, // highlight-end "body": "\r\n--graphql\r\ncontent-type: application/json\r\n\r\n{\"data\":{\"me\":{\"id\":\"1\"}},\"hasNext\":true}\r\n--graphql\r\n", "context": { "entries": { "operation_kind": "query", - "apollo_telemetry::client_version": "", - "apollo_telemetry::client_name": "manual" + "apollo::telemetry::client_version": "", + "apollo::telemetry::client_name": "manual" } }, "statusCode": 200 //highlight-line @@ -1220,8 +1190,8 @@ Subsequent response chunks omit the `headers` and `statusCode` fields: "context": { "entries": { "operation_kind": "query", - "apollo_telemetry::client_version": "", - "apollo_telemetry::client_name": "manual" + "apollo::telemetry::client_version": "", + "apollo::telemetry::client_name": "manual" } } } @@ -1246,15 +1216,15 @@ This configuration prompts the router to send an HTTP POST request to your copro ```json { - "version": 1, - "stage": "RouterRequest", - "control": "continue", - "id": "d0a8245df0efe8aa38a80dba1147fb2e", - "context": { - "entries": { - "accepts-json": true - } + "version": 1, + "stage": "RouterRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "accepts-json": true } + } } ``` @@ -1262,18 +1232,18 @@ When your coprocessor receives this request from the router, it should add claim ```json { - "version": 1, - "stage": "RouterRequest", - "control": "continue", - "id": "d0a8245df0efe8aa38a80dba1147fb2e", - "context": { - "entries": { - "accepts-json": true, - "apollo_authentication::JWT::claims": { - "scope": "profile:read profile:write" - } - } + "version": 1, + "stage": "RouterRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "accepts-json": true, + "apollo_authentication::JWT::claims": { + "scope": "profile:read profile:write" + } } + } } ``` @@ -1286,4 +1256,4 @@ When your coprocessor receives this request from the router, it should add claim - [`@policy` coprocessor example](https://github.com/apollosolutions/example-coprocessor-auth-policy) - Use the Apollo Solutions [router extensibility load testing repository](https://github.com/apollosolutions/router-extensibility-load-testing) to load test coprocessors. - \ No newline at end of file + diff --git a/docs/source/routing/customization/overview.mdx b/docs/source/routing/customization/overview.mdx index c417b06d19a..bd2da1916e6 100644 --- a/docs/source/routing/customization/overview.mdx +++ b/docs/source/routing/customization/overview.mdx @@ -11,10 +11,10 @@ You can create **customizations** for the GraphOS Router or Apollo Router Core t The GraphOS Router supports the following customization types: - [**Rhai scripts**](/graphos/routing/customization/rhai/) - - The [Rhai scripting language](https://rhai.rs/book/) lets you add functionality directly to your stock router binary by hooking into different phases of the router's request lifecycle. + - The [Rhai scripting language](https://rhai.rs/book/) lets you add functionality directly to your stock router binary by hooking into different phases of the router's request lifecycle. - [**External co-processing**](/router/customizations/coprocessor/) ([Enterprise feature](/router/enterprise-features/)) - - If your organization has a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/), you can write custom request-handling code in any language. This code can run in the same container as your router or separately. - - The router calls your custom code via HTTP, passing it the details of each incoming client request. + - If your organization has a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/), you can write custom request-handling code in any language. This code can run in the same container as your router or separately. + - The router calls your custom code via HTTP, passing it the details of each incoming client request. The Apollo Router Core supports customization only through [Rhai scripts](/graphos/routing/customization/rhai/). @@ -49,7 +49,7 @@ flowchart RL api1("subgraph A"); api2("subgraph B"); api3("subgraph C"); - api1 --- api2 --- api3 + api1 --- api2 --- api3 end @@ -76,7 +76,7 @@ For responses, the router executes the plugins _after_ the service. ```mermaid flowchart RL subgraph Service - coreService["Core
service"] -->|response| Plugin2["Plugin 2"] -->|response| Plugin1["Plugin 1"] + coreService["Core
service"] -->|response| Plugin2["Plugin 2"] -->|response| Plugin1["Plugin 1"] end Plugin1["Plugin 1"] -->|response| Client @@ -106,8 +106,8 @@ flowchart TB; end queryPlanner("Query Planner"); end - - + + subgraph executionService["Execution Service"] executionPlugins[[Execution plugins]]; end @@ -118,7 +118,7 @@ flowchart TB; end subgraph service2["Subgraph Service B"] subgraphPlugins2[[Subgraph plugins]]; - end + end end end; subgraphA[Subgraph A]; @@ -138,7 +138,7 @@ service1 --"8a. HTTP request"--> subgraphA; service2 --"8b. HTTP request"--> subgraphB; ``` -1. The router receives a client request at an HTTP server. +1. The router receives a client request at an HTTP server. 2. The HTTP server transforms the HTTP request into a `RouterRequest` containing HTTP headers and the request body as a stream of byte arrays. 3. The router service receives the `RouterRequest`. It handles Automatic Persisted Queries (APQ), parses the GraphQL request from JSON, and calls the supergraph service with the resulting `SupergraphRequest`. 4. The supergraph service calls the query planner with the GraphQL query from the `SupergraphRequest`. @@ -146,10 +146,10 @@ service2 --"8b. HTTP request"--> subgraphB; 6. The supergraph service calls the execution service with an `ExecutionRequest`, made up of `SupergraphRequest` and the query plan. 7. For each fetch node of the query plan, the execution service creates a `SubgraphRequest` and then calls the respective subgraph service. 8. Each subgraph has its own subgraph service, and each service can have its own subgraph plugin configuration. The subgraph service transforms the `SubgraphRequest` into an HTTP request to its subgraph. The `SubgraphRequest` contains: - - the (read-only) `SupergraphRequest` - - HTTP headers - - the subgraph request's operation type (query, mutation, or subscription) - - a GraphQL request object as the request body + - the (read-only) `SupergraphRequest` + - HTTP headers + - the subgraph request's operation type (query, mutation, or subscription) + - a GraphQL request object as the request body Once your subgraphs provide a response, the response follows the path outlined below. @@ -170,8 +170,8 @@ flowchart BT; end queryPlanner("QueryPlanner"); end - - + + subgraph executionService["Execution Service"] executionPlugins[[Execution plugins]]; end @@ -182,7 +182,7 @@ flowchart BT; end subgraph service2["Subgraph Service B"] subgraphPlugins2[[Subgraph plugins]]; - end + end end end; subgraph1[Subgraph A]; @@ -211,7 +211,7 @@ For simplicity's sake, the preceding diagrams show the request and response side For example, `SubgraphRequest`s can happen both in parallel _and_ in sequence: one subgraph's response may be necessary for another's `SubgraphRequest`. (The query planner decides which requests can happen in parallel vs. which need to happen in sequence). To match subgraph requests to responses in customizations, the router exposes a `subgraph_request_id` field that will hold the same value in paired requests and responses. -##### Requests run in parallel +##### Requests run in parallel ```mermaid flowchart LR; @@ -226,7 +226,7 @@ flowchart LR; end subgraph service2["Subgraph Service B"] subgraphPlugins2[[Subgraph plugins]]; - end + end end @@ -259,7 +259,7 @@ flowchart LR; end subgraph service2["Subgraph Service B"] subgraphPlugins2[[Subgraph plugins]]; - end + end end @@ -283,15 +283,46 @@ Additionally, some requests and responses may happen multiple times for the same The router expects to execute on a stream of data. In order to work correctly and provide high performance, the following expectations must be met: -* **Request Path**: No buffering before the end of the `router_service` processing step -* **Response Path**: No buffering +- **Request Path**: No buffering before the end of the `router_service` processing step +- **Response Path**: No buffering > In general, it's best to avoid buffering where possible. If necessary, it is ok to do so on the request path once the `router_service` step is complete. This guidance applies if you are: - - Modifying the router - - Creating a native Rust plugin - - Creating a custom binary + +- Modifying the router +- Creating a native Rust plugin +- Creating a custom binary + +### Request Context + +The router makes several values available in the request context, which is shared across stages of the processing pipeline. + +- `apollo::apq::cache_hit`: present if the request used APQ, true if we got a cache hit for the query id, false otherwise +- `apollo::apq::registered`: true if the request registered a query in APQ +- `apollo::authentication::jwt_claims`: claims extracted from a JWT if present in the request +- `apollo::authorization::authenticated_required`: true if the query covers type of fields marked with `@authenticated` +- `apollo::authorization::required_policies`: if the query covers type of fields marked with `@policy`, it contains a map of `policy name -> Option`. A coprocessor or rhai script can edit this map to mark `true` on authorization policies that succeed or `false` on ones that fail +- `apollo::authorization::required_scopes`: if the query covers type of fields marked with `@requiresScopes`, it contains the list of scopes used by those directive applications +- `apollo::demand_control::actual_cost`: calculated cost of the responses returned by the subgraphs; populated by the demand control plugin +- `apollo::demand_control::estimated_cost`: estimated cost of the requests to be sent to the subgraphs; populated by the demand control plugin +- `apollo::demand_control::result`: `COST_OK` if allowed, and `COST_TOO_EXPENSIVE` if rejected due to cost limits; populated by the demand control plugin +- `apollo::demand_control::strategy`: the name of the cost calculation strategy used by the demand control plugin +- `apollo::entity_cache::cached_keys_status`: a map of cache control statuses for cached entities, keyed by subgraph request id; populated by the entity caching plugin when `expose_keys_in_context` is turned on in the router configuration +- `apollo::expose_query_plan::enabled`: true if experimental query plan exposure is enabled +- `apollo::expose_query_plan::formatted_plan`: query plan formatted as text +- `apollo::expose_query_plan::plan`: contains the query plan serialized as JSON (editing it has no effect on execution) +- `apollo::progressive_override::labels_to_override`: used in progressive override, list of labels for which we need an override +- `apollo::progressive_override::unresolved_labels`: used in progressive override, contains the list of unresolved labels +- `apollo::supergraph::first_event`: false if the current response chunk is not the first response in the stream, nonexistent otherwise +- `apollo::supergraph::operation_id`: contains the usage reporting stats report key +- `apollo::supergraph::operation_kind`: can be `Query`, `Mutation` or `Subscription` +- `apollo::supergraph::operation_name`: name of the operation (according to the query and the operation_name field in the request) +- `apollo::telemetry::client_name`: client name extracted from the client name header +- `apollo::telemetry::client_version`: client version extracted from the client version header +- `apollo::telemetry::contains_graphql_error`: true if the response contains at least one error +- `apollo::telemetry::studio_exclude`: true if the current request's trace details should be excluded from Studio +- `apollo::telemetry::subgraph_ftv1`: JSON-serialized trace data returned by the subgraph when FTV1 is enabled ## Customization creation diff --git a/docs/source/routing/graphos-reporting.mdx b/docs/source/routing/graphos-reporting.mdx index d68ee0e5037..eee7d5217aa 100644 --- a/docs/source/routing/graphos-reporting.mdx +++ b/docs/source/routing/graphos-reporting.mdx @@ -75,7 +75,7 @@ Your subgraph libraries must support federated tracing (also known as FTV1 traci - To confirm support, check the `FEDERATED TRACING` entry for your library on [this page](/federation/building-supergraphs/supported-subgraphs). - Consult your library's documentation to learn how to enable federated tracing. - - If you use Apollo Server with `@apollo/subgraph`, federated tracing support is enabled automatically. + - If you use Apollo Server with `@apollo/subgraph`, federated tracing support is enabled automatically. ### Subgraph trace sampling @@ -86,7 +86,7 @@ You can customize your router's trace sampling probability by setting the follow ```yaml title="router.yaml" telemetry: apollo: - # In this example, the trace sampler is configured + # In this example, the trace sampler is configured # with a 50% probability of sampling a request. # This value can't exceed the value of tracing.common.sampler. field_level_instrumentation_sampler: 0.5 @@ -330,7 +330,6 @@ An array of names for the variables that the router _will not_ report to GraphOS - ```yaml title="router.yaml" telemetry: apollo: @@ -366,6 +365,7 @@ By default, your router _does_ report error information, and it _does_ redact th Your subgraph libraries must support federated tracing (also known as FTV1 tracing) to provide errors to GraphOS. If you use Apollo Server with `@apollo/subgraph`, federated tracing support is enabled automatically. To confirm support: + - Check the `FEDERATED TRACING` entry for your library on [the supported subgraphs page](/federation/building-supergraphs/supported-subgraphs). - If federated tracing isn't enabled automatically for your library, consult its documentation to learn how to enable it. - Note that federated tracing can also be sampled (see above) so error messages might not be available for all your operations if you have sampled to a lower level. @@ -386,9 +386,8 @@ telemetry: send: false ``` - -If you're writing a plugin, you can get the Studio Trace ID by reading the value of `apollo_operation_id` from the context. +If you're writing a plugin, you can get the Studio Trace ID by reading the value of `apollo::supergraph::operation_id` from the context. diff --git a/docs/source/routing/security/authorization.mdx b/docs/source/routing/security/authorization.mdx index 94a6ed356c2..90b09e66fd2 100644 --- a/docs/source/routing/security/authorization.mdx +++ b/docs/source/routing/security/authorization.mdx @@ -38,6 +38,7 @@ Services may have their own access controls, but enforcing authorization _in the router -.->|"❌ Unauthorized
subquery"| serviceB; clients -->|"⚠️ Partially authorized
request"| router; ``` + - Also, [query deduplication](/router/configuration/traffic-shaping/#query-deduplication) groups requested fields based on their required authorization. Entire groups can be eliminated from the query plan if they don't have the correct authorization. - **Declarative access rules**: You define access controls at the field level, and GraphOS [composes](#composition-and-federation) them across your services. These rules create graph-native governance without the need for an extra orchestration layer. @@ -111,6 +112,7 @@ Only the GraphOS Router supports authorization directives—[`@apollo/gatewa Before using the authorization directives in your subgraph schemas, you must: + - Validate that your GraphOS Router uses version `1.29.1` or later and is [connected to your GraphOS Enterprise organization](/router/enterprise-features/#enabling-enterprise-features) - Include **[claims](#configure-request-claims)** in requests made to the router (for `@authenticated` and `@requiresScopes`) @@ -121,7 +123,7 @@ Claims are the individual details of a request's authentication and scope. They To provide the router with the claims it needs, you must either configure JSON Web Token (JWT) authentication or add an external coprocessor that adds claims to a request's context. In some cases (explained below), you may require both. - **JWT authentication configuration**: If you configure [JWT authentication](/router/configuration/authn-jwt), the GraphOS Router [automatically adds a JWT token's claims](/router/configuration/authn-jwt#working-with-jwt-claims) to the request's context at the `apollo_authentication::JWT::claims` key. -- **Adding claims via coprocessor**: If you can't use JWT authentication, you can [add claims with a coprocessor](/router/customizations/coprocessor#adding-authorization-claims-via-coprocessor). Coprocessors let you hook into the GraphOS Router's request-handling lifecycle with custom code. +- **Adding claims via coprocessor**: If you can't use JWT authentication, you can [add claims with a coprocessor](/router/customizations/coprocessor#adding-authorization-claims-via-coprocessor). Coprocessors let you hook into the GraphOS Router's request-handling lifecycle with custom code. - **Augmenting JWT claims via coprocessor**: Your authorization policies may require information beyond what your JSON web tokens provide. For example, a token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can [augment the claims](/router/configuration/authn-jwt#claim-augmentation-via-coprocessors) from your JSON web tokens with coprocessors. ## Authorization directives @@ -140,7 +142,7 @@ authorization: -The `@requiresScopes` directive marks fields and types as restricted based on required scopes. +The `@requiresScopes` directive marks fields and types as restricted based on required scopes. The directive includes a `scopes` argument with an array of the required scopes to declare which scopes are required: ```graphql @@ -151,7 +153,7 @@ The directive includes a `scopes` argument with an array of the required scopes Use `@requiresScopes` when access to a field or type depends only on claims associated with a claims object or access token. -If your authorization validation logic or data are more complex—such as checking specific values in headers or looking up data from other sources such as databases—and aren't solely based on a claims object or access token, use [`@policy`](#policy) instead. +If your authorization validation logic or data are more complex—such as checking specific values in headers or looking up data from other sources such as databases—and aren't solely based on a claims object or access token, use [`@policy`](#policy) instead. @@ -215,7 +217,9 @@ It is defined as follows: ```graphql scalar federation__Scope -directive @requiresScopes(scopes: [[federation__Scope!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM +directive @requiresScopes( + scopes: [[federation__Scope!]!]! +) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` #### Combining required scopes with `AND`/`OR` logic @@ -244,7 +248,6 @@ You can nest arrays and elements as needed to achieve your desired logic. For ex This syntax requires requests to have either (`scope1` **AND** `scope2`) **OR** just `scope3` to be authorized. - #### Example `@requiresScopes` use case Imagine the social media platform you're building lets users view other users' information only if they have the required permissions. @@ -308,26 +311,21 @@ The router returns `null` for unauthorized fields and applies the [standard Grap { "data": { "me": null, - "post": { - "title": "Securing supergraphs", - } + "post": { + "title": "Securing supergraphs" + } }, "errors": [ { "message": "Unauthorized field or type", - "path": [ - "me" - ], + "path": ["me"], "extensions": { "code": "UNAUTHORIZED_FIELD_OR_TYPE" } }, { "message": "Unauthorized field or type", - "path": [ - "post", - "views" - ], + "path": ["post", "views"], "extensions": { "code": "UNAUTHORIZED_FIELD_OR_TYPE" } @@ -392,7 +390,6 @@ type Post { content: String! views: Int @authenticated #highlight-line } - ``` Consider the following query: @@ -443,26 +440,21 @@ The response retains the initial request's shape but returns `null` for unauthor { "data": { "me": null, - "post": { - "title": "Securing supergraphs", - } + "post": { + "title": "Securing supergraphs" + } }, "errors": [ { "message": "Unauthorized field or type", - "path": [ - "me" - ], + "path": ["me"], "extensions": { "code": "UNAUTHORIZED_FIELD_OR_TYPE" } }, { "message": "Unauthorized field or type", - "path": [ - "post", - "views" - ], + "path": ["post", "views"], "extensions": { "code": "UNAUTHORIZED_FIELD_OR_TYPE" } @@ -497,12 +489,12 @@ Using the `@policy` directive requires a [Supergraph plugin](/router/customizati An overview of how `@policy` is processed through the router's request lifecycle: -* At the [`RouterService` level](/router/customizations/overview#the-request-lifecycle), the GraphOS Router extracts the list of policies relevant to a request from the schema and then stores them in the request's context in `apollo_authorization::policies::required` as a map `policy -> null|true|false`. +- At the [`RouterService` level](/router/customizations/overview#the-request-lifecycle), the GraphOS Router extracts the list of policies relevant to a request from the schema and then stores them in the request's context in `apollo::authorization::required_policies` as a map `policy -> null|true|false`. -* At the `SupergraphService` level, you must provide a Rhai script or coprocessor to evaluate the map. -If the policy is validated, the script or coprocessor should set its value to `true` or otherwise set it to `false`. If the value is left to `null`, it will be treated as `false` by the router. Afterward, the router filters the requests' types and fields to only those where the policy is `true`. +- At the `SupergraphService` level, you must provide a Rhai script or coprocessor to evaluate the map. + If the policy is validated, the script or coprocessor should set its value to `true` or otherwise set it to `false`. If the value is left to `null`, it will be treated as `false` by the router. Afterward, the router filters the requests' types and fields to only those where the policy is `true`. -* If no field of a subgraph query passes its authorization policies, the router stops further processing of the query and precludes unauthorized subgraph requests. This efficiency gain is a key benefit of the `@policy` and other authorization directives. +- If no field of a subgraph query passes its authorization policies, the router stops further processing of the query and precludes unauthorized subgraph requests. This efficiency gain is a key benefit of the `@policy` and other authorization directives. #### Usage @@ -519,7 +511,9 @@ The `@policy` directive is defined as follows: ```graphql scalar federation__Policy -directive @policy(policies: [[federation__Policy!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM +directive @policy( + policies: [[federation__Policy!]!]! +) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` Using the `@policy` directive requires a [Supergraph plugin](/router/customizations/overview) to evaluate the authorization policies. You can do this with a [Rhai script](/graphos/routing/customization/rhai/) or [coprocessor](/router/customizations/coprocessor). Refer to the following [example use case](#example-policy-use-case) for more information. (Although a [native plugin](/router/customizations/native) can also evaluate authorization policies, we don't recommend using it.) @@ -573,7 +567,6 @@ type Post { content: String! views: Int @authenticated } - ``` You can use a [coprocessor](/router/customizations/coprocessor) called at the Supergraph request stage to receive and execute the list of policies. @@ -592,23 +585,23 @@ A coprocessor can then receive a request with this format: ```json { - "version": 1, - "stage": "SupergraphRequest", - "control": "continue", - "id": "d0a8245df0efe8aa38a80dba1147fb2e", - "context": { - "entries": { - "apollo_authentication::JWT::claims": { - "exp": 10000000000, - "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" - }, - "apollo_authorization::policies::required": { - "read_profile": null, - "read_credit_card": null - } - } - }, - "method": "POST" + "version": 1, + "stage": "SupergraphRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "apollo_authentication::JWT::claims": { + "exp": 10000000000, + "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" + }, + "apollo::authorization::required_policies": { + "read_profile": null, + "read_credit_card": null + } + } + }, + "method": "POST" } ``` @@ -616,28 +609,28 @@ A user can read their own profile, so `read_profile` will succeed. But only the ```json { - "version": 1, - "stage": "SupergraphRequest", - "control": "continue", - "id": "d0a8245df0efe8aa38a80dba1147fb2e", - "context": { - "entries": { - "apollo_authentication::JWT::claims": { - "exp": 10000000000, - "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" - }, - "apollo_authorization::policies::required": { - "read_profile": true, - "read_credit_card": false - } - } + "version": 1, + "stage": "SupergraphRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "apollo_authentication::JWT::claims": { + "exp": 10000000000, + "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" + }, + "apollo::authorization::required_policies": { + "read_profile": true, + "read_credit_card": false + } } + } } ``` ##### Usage with a Rhai script -For another example, suppose that you want to restrict access for posts to a support user. Given that the `policies` argument is a string, you can set it as a `":"` format that a Rhai script can parse and evaluate. +For another example, suppose that you want to restrict access for posts to a support user. Given that the `policies` argument is a string, you can set it as a `":"` format that a Rhai script can parse and evaluate. The relevant part of your schema may look like this: @@ -658,7 +651,7 @@ You can then use the following Rhai script to parse and evaluate the `policies` fn supergraph_service(service) { let request_callback = |request| { let claims = request.context["apollo_authentication::JWT::claims"]; - let policies = request.context["apollo_authorization::policies::required"]; + let policies = request.context["apollo::authorization::required_policies"]; if policies != () { for key in policies.keys() { @@ -676,7 +669,7 @@ fn supergraph_service(service) { } } } - request.context["apollo_authorization::policies::required"] = policies; + request.context["apollo::authorization::required_policies"] = policies; }; service.map_request(request_callback); } @@ -695,7 +688,6 @@ GraphOS's composition strategy for authorization directives is intentionally acc If a shared field uses different authorization directives across subgraphs, composition merges them using `AND` logic. For example, suppose the `me` query requires `@authenticated` in one subgraph and the `read:user` scope in another subgraph: - ```graphql title="Subgraph A" type Query { me: User @authenticated @@ -763,8 +755,8 @@ Refer to the section on [Combining policies with AND/OR logic](#combining-polici Using **OR** logic for shared directives simplifies schema updates. If requirements change suddenly, you don't need to update the directive in all subgraphs simultaneously. - #### Combining `AND`/`OR` logic with `@requiresScopes` + As with [combining scopes for a single use of [`@requiresScopes`](#combining-required-scopes-with-andor-logic), you can use nested arrays to introduce **AND** logic in a single subgraph: ```graphql title="Subgraph A" @@ -783,7 +775,8 @@ Since both subgraphs use the same authorization directive, composition [merges t ```graphql title="Supergraph" type Query { - users: [User!]! @requiresScopes(scopes: [["read:others", "read:users"], ["read:profiles"]]) + users: [User!]! + @requiresScopes(scopes: [["read:others", "read:users"], ["read:profiles"]]) } ``` diff --git a/examples/coprocessor-override-launchdarkly/src/index.ts b/examples/coprocessor-override-launchdarkly/src/index.ts index b66dcbbd8f5..6518e11104f 100644 --- a/examples/coprocessor-override-launchdarkly/src/index.ts +++ b/examples/coprocessor-override-launchdarkly/src/index.ts @@ -3,8 +3,8 @@ import express from "express"; import { listenForFlagUpdates } from "./launchDarkly.js"; const LABEL_PREFIX = "launchDarkly:"; -const UNRESOLVED_LABELS_CONTEXT_KEY = "apollo_override::unresolved_labels"; -const LABELS_TO_OVERRIDE_CONTEXT_KEY = "apollo_override::labels_to_override"; +const UNRESOLVED_LABELS_CONTEXT_KEY = "apollo::progressive_override::unresolved_labels"; +const LABELS_TO_OVERRIDE_CONTEXT_KEY = "apollo::progressive_override::labels_to_override"; const { PORT } = process.env; diff --git a/examples/coprocessor-subgraph/rust/src/echo_co_processor.rs b/examples/coprocessor-subgraph/rust/src/echo_co_processor.rs index 5fa7cd3a309..de1ac74be63 100644 --- a/examples/coprocessor-subgraph/rust/src/echo_co_processor.rs +++ b/examples/coprocessor-subgraph/rust/src/echo_co_processor.rs @@ -116,7 +116,7 @@ impl Service for SimpleEndpoint { let context = context.get_mut("entries").unwrap(); // context always has entries. if let Some(context) = context.as_object_mut() { context.insert( - "apollo_authentication::JWT::claims".to_string(), + "apollo::authentication::jwt_claims".to_string(), json! { true }, ); } @@ -126,7 +126,7 @@ impl Service for SimpleEndpoint { "context".to_string(), json! {{ "entries": { - "apollo_authentication::JWT::claims": true + "apollo::authentication::jwt_claims": true } }}, )