diff --git a/.changesets/feat_bryn_demand_control.md b/.changesets/feat_bryn_demand_control.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.config/nextest.toml b/.config/nextest.toml index 828949aecf..df8bab66e6 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -9,7 +9,7 @@ final-status-level = "skip" # Do not cancel the test run on the first failure. fail-fast = false -# Most tests should take much less than 2 minute (see override below) +# Each test should take much less than 2 minute slow-timeout = { period = "30s", terminate-after = 4 } # Write to output for persistence to CircleCI @@ -19,12 +19,6 @@ path = "junit.xml" # Integration tests require more than one thread. The default setting of 1 will cause too many integration tests to run # at the same time and causes tests to fail where timing is involved. # This filter applies only to to the integration tests in the apollo-router package. -[[profile.default.overrides]] -filter = 'package(apollo-router) & kind(test)' -threads-required = 2 - -# Scaffold test takes all the test threads as it runs rustc, and takes more time. -[[profile.default.overrides]] -filter = 'package(apollo-router-scaffold)' -threads-required = 'num-test-threads' -slow-timeout = { period = "60s", terminate-after = 10 } +[[profile.ci.overrides]] +filter = 'test(/^apollo-router::/)' +threads-required = 4 diff --git a/CHANGELOG.md b/CHANGELOG.md index b8b6339055..49e3553c40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,216 @@ All notable changes to Router will be documented in this file. This project adheres to [Semantic Versioning v2.0.0](https://semver.org/spec/v2.0.0.html). +# [1.48.0] - 2024-05-29 + +## 🚀 Features + +### Demand control preview ([PR #5317](https://github.com/apollographql/router/pull/5317)) + +> ⚠️ This is a preview for an [Enterprise feature](https://www.apollographql.com/blog/platform/evaluating-apollo-router-understanding-free-and-open-vs-commercial-features/) of the Apollo Router. It requires an organization with a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/). If your organization doesn't currently have an Enterprise plan, you can test out this functionality with a [free Enterprise trial](https://studio.apollographql.com/signup?type=enterprise-trial). +> +> As a preview feature, it's subject to our [Preview launch stage](https://www.apollographql.com/docs/resources/product-launch-stages/#preview) expectations and configuration and performance may change in future releases. + +Demand control allows you to control the cost of operations in the router, potentially rejecting requests that are too expensive that could bring down the Router or subgraphs. + +```yaml +# Demand control enabled, but in measure mode. +preview_demand_control: + enabled: true + # `measure` or `enforce` mode. Measure mode will analyze cost of operations but not reject them. + mode: measure + + strategy: + # Static estimated strategy has a fixed cost for elements and when set to enforce will reject + # requests that are estimated as too high before any execution takes place. + static_estimated: + # The assumed returned list size for operations. This should be set to the maximum number of items in graphql list + list_size: 10 + # The maximum cost of a single operation. + max: 1000 +``` + +Telemetry is emitted for demand control, including the estimated cost of operations and whether they were rejected or not. +Full details will be included in the documentation for demand control which will be finalized before the next release. + +By [@bryncooke](https://github.com/bryncooke) in https://github.com/apollographql/router/pull/5317 + +### Ability to include Apollo Studio trace ID on tracing spans ([Issue #3803](https://github.com/apollographql/router/issues/3803)), ([Issue #5172](https://github.com/apollographql/router/issues/5172)) + +Add support for a new trace ID selector kind, the `apollo` trace ID, which represents the trace ID on [Apollo GraphOS Studio](https://studio.apollographql.com/). + +An example configuration using `trace_id: apollo`: + +```yaml +telemetry: + instrumentation: + spans: + router: + "studio.trace.id": + trace_id: apollo +``` + + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/5189 + +### Add ability for router to deal with query plans with contextual rewrites ([PR #5097](https://github.com/apollographql/router/pull/5097)) + +Adds the ability for the router to execute query plans with context rewrites. A context is generated by the `@fromContext` directive, and each context maps values in the collected data JSON onto a variable that's used as an argument to a field resolver. To learn more, see [Saving and referencing data with contexts](https://www.apollographql.com/docs/federation/federated-types/federated-directives#saving-and-referencing-data-with-contexts). + +⚠️ Because this feature requires a new version of federation, v2.8.0, distributed caches will need to be repopulated. + +By [@clenfest](https://github.com/clenfest) in https://github.com/apollographql/router/pull/5097 + +## 🐛 Fixes + +### Fix custom attributes for spans and histogram when used with `response_event` ([PR #5221](https://github.com/apollographql/router/pull/5221)) + +This release fixes multiple issues related to spans and selectors: + +- Custom attributes based on response_event in spans are properly added. +- Histograms using response_event selectors are properly updated. +- Static selectors that set a static value are now able to take a Value. +- Static selectors that set a static value are now set at every stage. +- The `on_graphql_error` selector is available on the supergraph stage. +- The status of a span can be overridden with the `otel.status_code` attribute. + +As an example of using these fixes, the configuration below uses spans with static selectors to mark spans as errors when GraphQL errors occur: + +```yaml +telemetry: + instrumentation: + spans: + router: + attributes: + otel.status_code: + static: error + condition: + eq: + - true + - on_graphql_error: true + supergraph: + attributes: + otel.status_code: + static: error + condition: + eq: + - true + - on_graphql_error: true +``` + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/5221 + +### Fix instrument incrementing on aborted request when condition is not fulfilled ([PR #5215](https://github.com/apollographql/router/pull/5215)) + +Previously when a telemetry instrument was dropped it would be incremented even if the associated condition was not fulfilled. For instance: + +```yaml +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit + # This instrument should not be triggered as the condition is never true + condition: + eq: + - response_header: "never-received" + - static: "true" +``` + +In the case where a request was started, but the client aborted the request before the response was sent, the `response_header` would never be set to `"never-received"`, +and the instrument would not be triggered. However, the instrument would still be incremented. + +Conditions are now checked for aborted requests, and the instrument is only incremented if the condition is fulfilled. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5215 + +## 🛠 Maintenance + +### Send query planner and lifecycle metrics to Apollo ([PR #5267](https://github.com/apollographql/router/pull/5267), [PR #5270](https://github.com/apollographql/router/pull/5270)) + +To enable the performance measurement of the router's new query planner implementation, the router transmits to Apollo the following new metrics: +- `apollo.router.query_planning.*` provides metrics on the query planner that help improve the query planning implementation. +- `apollo.router.lifecycle.api_schema` provides feedback on the experimental Rust-based API schema generation. +- `apollo.router.lifecycle.license` provides metrics on license expiration that help improve the reliability of the license check mechanism. + +These metrics don't leak any sensitive data. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5267, [@goto-bus-stop](https://github.com/goto-bus-stop) + +## 📚 Documentation + +### Add Rhai API constants reference + +The Rhai API documentation now includes [a list of available constants](https://www.apollographql.com/docs/router/customizations/rhai-api/#available-constants) that are available in the Rhai runtime. + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/5189 +## 🧪 Experimental + +### GraphQL instruments ([PR #5215](https://github.com/apollographql/router/pull/5215), [PR #5257](https://github.com/apollographql/router/pull/5257)) + +This PR adds experimental GraphQL instruments to telemetry. + +The new instruments are configured in the following: +``` +telemetry: + instrumentation: + instruments: + graphql: + # The number of times a field was executed (counter) + field.execution: true + + # The length of list fields (histogram) + list.length: true + + # Custom counter of field execution where field name = name + "custom_counter": + description: "count of name field" + type: counter + unit: "unit" + value: field_unit + attributes: + graphql.type.name: true + graphql.field.type: true + graphql.field.name: true + condition: + eq: + - field_name: string + - "name" + + # Custom histogram of list lengths for topProducts + "custom_histogram": + description: "histogram of review length" + type: histogram + unit: "unit" + attributes: + graphql.type.name: true + graphql.field.type: true + graphql.field.name: true + value: + field_custom: + list_length: value + condition: + eq: + - field_name: string + - "topProducts" +``` + +Using the new instruments consumes significant performance resources from the router. Their performance will be improved in a future release. + +Large numbers of metrics may also be generated by using the instruments, so make sure to not incur excessively large APM costs. + +⚠ Use these instruments only in development. Don't use them in production. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5215 and https://github.com/apollographql/router/pull/5257 + + + # [1.47.0] - 2024-05-21 ## 🚀 Features diff --git a/Cargo.lock b/Cargo.lock index 170c319844..17e6a28d16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,7 +220,7 @@ dependencies = [ [[package]] name = "apollo-federation" -version = "1.47.0" +version = "1.48.0" dependencies = [ "apollo-compiler", "derive_more", @@ -262,7 +262,7 @@ dependencies = [ [[package]] name = "apollo-router" -version = "1.47.0" +version = "1.48.0" dependencies = [ "access-json", "anyhow", @@ -425,7 +425,7 @@ dependencies = [ [[package]] name = "apollo-router-benchmarks" -version = "1.47.0" +version = "1.48.0" dependencies = [ "apollo-parser", "apollo-router", @@ -441,18 +441,36 @@ dependencies = [ [[package]] name = "apollo-router-scaffold" -version = "1.47.0" +version = "1.48.0" dependencies = [ "anyhow", "cargo-scaffold", "clap", "copy_dir", + "dircmp", "regex", + "similar", "str_inflector", "tempfile", "toml", ] +[[package]] +name = "apollo-router-scaffold-test" +version = "0.1.0" +dependencies = [ + "anyhow", + "apollo-router", + "async-trait", + "futures", + "schemars", + "serde", + "serde_json", + "tokio", + "tower", + "tracing", +] + [[package]] name = "apollo-smith" version = "0.5.0" @@ -1356,9 +1374,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bytes-utils" @@ -1393,9 +1411,9 @@ dependencies = [ [[package]] name = "cargo-scaffold" -version = "0.11.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ca5674d1f48a1788aae6124e87949474ab1560efb1a02a0f63a0ce9e845f0a" +checksum = "ad9211604c79bf86afd55f798b3c105607f87bd08a9edbf71b22785b0d53f851" dependencies = [ "anyhow", "auth-git2", @@ -1922,9 +1940,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f711ade317dd348950a9910f81c5947e3d8907ebd2b83f76203ff1807e6a2bc2" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" dependencies = [ "cfg-if", "cpufeatures", @@ -2287,6 +2305,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "dircmp" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ca7fa3ba397980657070e679f412acddb7a372f1793ff68ef0bbe708680f0f" +dependencies = [ + "regex", + "sha2", + "thiserror", + "walkdir", +] + [[package]] name = "directories" version = "5.0.1" @@ -2389,7 +2419,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve 0.13.8", "rfc6979 0.4.0", - "signature 2.0.0", + "signature 2.2.0", "spki 0.7.2", ] @@ -2629,9 +2659,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.1.20" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" @@ -3924,9 +3954,9 @@ dependencies = [ [[package]] name = "libz-ng-sys" -version = "1.1.12" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dd9f43e75536a46ee0f92b758f6b63846e594e86638c61a9251338a65baea63" +checksum = "c6409efc61b12687963e602df8ecf70e8ddacf95bc6576bcf16e3ac6328083c5" dependencies = [ "cmake", "libc", @@ -4120,9 +4150,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] @@ -5165,7 +5195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit 0.19.14", + "toml_edit 0.19.15", ] [[package]] @@ -5746,9 +5776,9 @@ dependencies = [ [[package]] name = "router-bridge" -version = "0.5.21+v2.7.5" +version = "0.5.25+v2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2142445fe3fe2aae7a3c3c5083d1211a448a0dabb489a14dd90d427cf6c0b13" +checksum = "9ecf6c55973979655b1aa34bbc0e62766f937c1f95c4eb3fa07d16f50cb4d63c" dependencies = [ "anyhow", "async-channel 1.9.0", @@ -5870,6 +5900,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cb0a25bfbb2d4b4402179c2cf030387d9990857ce08a32592c6238db9fa8665" dependencies = [ + "globset", "sha2", "walkdir", ] @@ -6006,9 +6037,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0afe01b987fac84253ce8acd5c05af9941975e4dee5b4f2d826b6947be8ec2c7" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "schemars_derive", @@ -6019,9 +6050,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d253e72f060451e9e5615a1686f3cb4ff87c4e70504c79bdab8fb3b010cd4e97" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2 1.0.76", "quote 1.0.35", @@ -6119,9 +6150,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" dependencies = [ "serde_derive", ] @@ -6137,9 +6168,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" dependencies = [ "proc-macro2 1.0.76", "quote 1.0.35", @@ -6171,9 +6202,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "indexmap 2.2.3", "itoa", @@ -6355,9 +6386,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", "rand_core 0.6.4", @@ -6365,9 +6396,9 @@ dependencies = [ [[package]] name = "similar" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" [[package]] name = "simple_asn1" @@ -7040,9 +7071,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.2.3", "toml_datetime", @@ -7342,11 +7373,10 @@ dependencies = [ [[package]] name = "tracing-test" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a2c0ff408fe918a94c428a3f2ad04e4afd5c95bbc08fcf868eff750c15728a4" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" dependencies = [ - "lazy_static", "tracing-core", "tracing-subscriber", "tracing-test-macro", @@ -7354,13 +7384,12 @@ dependencies = [ [[package]] name = "tracing-test-macro" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bc1c4f8e2e73a977812ab339d503e6feeb92700f6d07a6de4d321522d5c08" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ - "lazy_static", "quote 1.0.35", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] @@ -7579,9 +7608,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-id" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d70b6494226b36008c8366c288d77190b3fad2eb4c10533139c1c1f461127f1a" +checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" [[package]] name = "unicode-ident" @@ -8175,7 +8204,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" dependencies = [ - "curve25519-dalek 4.0.0", + "curve25519-dalek 4.1.2", "rand_core 0.6.4", "serde", "zeroize", diff --git a/Cargo.toml b/Cargo.toml index c4d946f031..134d321297 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "apollo-router", "apollo-router-benchmarks", "apollo-router-scaffold", + "apollo-router-scaffold/scaffold-test", "apollo-federation", "apollo-federation/cli", "examples/add-timestamp-header/rhai", diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index 48f1368180..8b5680694d 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-federation" -version = "1.47.0" +version = "1.48.0" authors = ["The Apollo GraphQL Contributors"] edition = "2021" description = "Apollo Federation" @@ -15,7 +15,7 @@ time = { version = "0.3.34", default-features = false, features = [ "local-offset", ] } derive_more = "0.99.17" -indexmap = "2.1.0" +indexmap = "2.2.3" lazy_static = "1.4.0" multimap = "0.10.0" petgraph = "0.6.4" diff --git a/apollo-federation/src/link/join_spec_definition.rs b/apollo-federation/src/link/join_spec_definition.rs index c2b011e244..bf8734a4e6 100644 --- a/apollo-federation/src/link/join_spec_definition.rs +++ b/apollo-federation/src/link/join_spec_definition.rs @@ -336,6 +336,10 @@ lazy_static! { Version { major: 0, minor: 3 }, Some(Version { major: 2, minor: 0 }), )); + definitions.add(JoinSpecDefinition::new( + Version { major: 0, minor: 5 }, + Some(Version { major: 2, minor: 8 }), + )); definitions }; diff --git a/apollo-federation/src/query_plan/display.rs b/apollo-federation/src/query_plan/display.rs index 8c91f3eac1..37042d08d7 100644 --- a/apollo-federation/src/query_plan/display.rs +++ b/apollo-federation/src/query_plan/display.rs @@ -83,6 +83,7 @@ impl FetchNode { operation_kind: _, input_rewrites: _, output_rewrites: _, + context_rewrites: _, } = self; state.write(format_args!("Fetch(service: {subgraph_name:?}"))?; if let Some(id) = id { diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index c23054a4e8..271cf95dc0 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -1320,6 +1320,7 @@ impl FetchDependencyGraphNode { operation_kind: self.root_kind.into(), input_rewrites: self.input_rewrites.clone(), output_rewrites, + context_rewrites: Default::default(), })); Ok(Some(if let Some(path) = self.merge_at.clone() { diff --git a/apollo-federation/src/query_plan/mod.rs b/apollo-federation/src/query_plan/mod.rs index 7a27999455..ce19b6cb7f 100644 --- a/apollo-federation/src/query_plan/mod.rs +++ b/apollo-federation/src/query_plan/mod.rs @@ -14,6 +14,7 @@ pub(crate) mod fetch_dependency_graph; pub(crate) mod fetch_dependency_graph_processor; pub mod generate; pub(crate) mod operation; +mod optimize; pub mod query_planner; pub(crate) mod query_planning_traversal; @@ -85,6 +86,9 @@ pub struct FetchNode { /// Similar to `input_rewrites`, but for optional "rewrites" to apply to the data that is /// received from a fetch (and before it is applied to the current in-memory results). pub output_rewrites: Vec>, + /// Similar to the other kinds of rewrites. This is a mechanism to convert a contextual path into + /// an argument to a resolver + pub context_rewrites: Vec>, } #[derive(Debug, Clone)] diff --git a/apollo-federation/src/query_plan/operation.rs b/apollo-federation/src/query_plan/operation.rs index dfcde55cab..d2946144d3 100644 --- a/apollo-federation/src/query_plan/operation.rs +++ b/apollo-federation/src/query_plan/operation.rs @@ -639,7 +639,7 @@ pub(crate) enum SelectionKey { } impl SelectionKey { - fn is_typename_field(&self) -> bool { + pub(crate) fn is_typename_field(&self) -> bool { matches!(self, SelectionKey::Field { response_name, .. } if *response_name == TYPENAME_FIELD) } } @@ -911,15 +911,32 @@ impl Selection { Selection::Field(field) => Ok(Selection::from( field.with_updated_selection_set(selection_set), )), - Selection::InlineFragment(inline_fragment) => Ok(Selection::from( - inline_fragment.with_updated_selection_set(selection_set), - )), + Selection::InlineFragment(inline_fragment) => { + let Some(selection_set) = selection_set else { + return Err(FederationError::internal( + "updating inline fragment without a sub-selection set", + )); + }; + Ok(inline_fragment + .with_updated_selection_set(selection_set) + .into()) + } Selection::FragmentSpread(_) => { Err(FederationError::internal("unexpected fragment spread")) } } } + pub(crate) fn with_updated_selections>( + &self, + type_position: CompositeTypeDefinitionPosition, + selections: impl IntoIterator, + ) -> Result { + let new_sub_selection = + SelectionSet::from_raw_selections(self.schema().clone(), type_position, selections); + self.with_updated_selection_set(Some(new_sub_selection)) + } + pub(crate) fn containment( &self, other: &Selection, @@ -1537,6 +1554,17 @@ impl FragmentSpreadSelection { }) } + pub(crate) fn from_fragment( + fragment: &Node, + directives: &executable::DirectiveList, + ) -> Self { + let spread_data = FragmentSpreadData::from_fragment(fragment, directives); + Self { + spread: FragmentSpread::new(spread_data), + selection_set: fragment.selection_set.clone(), + } + } + pub(crate) fn normalize( &self, parent_type: &CompositeTypeDefinitionPosition, @@ -1649,14 +1677,10 @@ mod normalized_inline_fragment_selection { } impl InlineFragmentSelection { - pub(crate) fn with_updated_selection_set( - &self, - selection_set: Option, - ) -> Self { + pub(crate) fn with_updated_selection_set(&self, selection_set: SelectionSet) -> Self { Self { inline_fragment: self.inline_fragment.clone(), - //FIXME - selection_set: selection_set.unwrap(), + selection_set, } } @@ -2007,7 +2031,7 @@ impl SelectionSet { SelectionSet::from_selection_set(&selection_set, &named_fragments, &schema) } - fn is_empty(&self) -> bool { + pub(crate) fn is_empty(&self) -> bool { self.selections.is_empty() } @@ -2500,6 +2524,7 @@ impl SelectionSet { schema: &ValidFederationSchema, parent_type: &CompositeTypeDefinitionPosition, selections: impl Iterator, + named_fragments: &NamedFragments, ) -> Result { let mut iter = selections; let Some(first) = iter.next() else { @@ -2514,7 +2539,7 @@ impl SelectionSet { return first .rebase_on( parent_type, - /*named_fragments*/ &Default::default(), + named_fragments, schema, RebaseErrorHandlingOption::ThrowError, )? @@ -2556,6 +2581,7 @@ impl SelectionSet { schema, sub_selection_parent_type, sub_selection_updates.values().map(|v| v.iter()), + named_fragments, )?); Selection::from_element(element, updated_sub_selection) } @@ -2564,14 +2590,15 @@ impl SelectionSet { /// - Assumes each item (slice) from the iterator has the same selection key within the slice. /// - Note that if the same selection key repeats in a later group, the previous group will be /// ignored and replaced by the new group. - fn make_selection_set<'a>( + pub(crate) fn make_selection_set<'a>( schema: &ValidFederationSchema, parent_type: &CompositeTypeDefinitionPosition, selection_key_groups: impl Iterator>, + named_fragments: &NamedFragments, ) -> Result { let mut result = SelectionMap::new(); for group in selection_key_groups { - let selection = Self::make_selection(schema, parent_type, group)?; + let selection = Self::make_selection(schema, parent_type, group, named_fragments)?; result.insert(selection); } Ok(SelectionSet { @@ -2642,6 +2669,7 @@ impl SelectionSet { &self.schema, &self.type_position, updated_selections.values().map(|v| v.iter()), + /*named_fragments*/ &Default::default(), ) } @@ -2741,7 +2769,12 @@ impl SelectionSet { let to_merge = [existing_selection, selection]; // `existing_selection` and `selection` both have the same selection key, // so the merged selection will also have the same selection key. - let selection = SelectionSet::make_selection(schema, parent_type, to_merge.iter())?; + let selection = SelectionSet::make_selection( + schema, + parent_type, + to_merge.iter(), + /*named_fragments*/ &Default::default(), + )?; selections.insert_at(index, selection); } None => { @@ -3222,6 +3255,15 @@ impl SelectionSet { } } +impl IntoIterator for SelectionSet { + type Item = as IntoIterator>::Item; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + Arc::unwrap_or_clone(self.selections).into_iter() + } +} + #[derive(Clone)] pub(crate) struct SelectionSetAtPath { path: Vec, @@ -3245,6 +3287,12 @@ pub(crate) struct CollectedFieldInSet { field: Arc, } +impl CollectedFieldInSet { + pub(crate) fn field(&self) -> &Arc { + &self.field + } +} + struct FieldInPath { path: Vec, field: Arc, @@ -3875,6 +3923,20 @@ impl Field { None } } + + pub(crate) fn parent_type_position(&self) -> CompositeTypeDefinitionPosition { + self.data().field_position.parent() + } + + pub(crate) fn types_can_be_merged(&self, other: &Self) -> Result { + let self_definition = self.data().field_position.get(self.schema().schema())?; + let other_definition = other.data().field_position.get(self.schema().schema())?; + types_can_be_merged( + &self_definition.ty, + &other_definition.ty, + self.schema().schema(), + ) + } } impl<'a> FragmentSpreadSelectionValue<'a> { @@ -4480,6 +4542,10 @@ impl NamedFragments { self.fragments.len() } + pub(crate) fn iter(&self) -> impl Iterator> { + self.fragments.values() + } + pub(crate) fn insert(&mut self, fragment: Fragment) { Arc::make_mut(&mut self.fragments).insert(fragment.name.clone(), Node::new(fragment)); } @@ -7148,6 +7214,7 @@ type T { &schema, &foo.element().unwrap().parent_type_position(), [foo_with_c, foo_with_b, foo_with_a].iter(), + /*named_fragments*/ &Default::default(), ) .unwrap(); // Make sure the ordering of c, b and a is preserved. diff --git a/apollo-federation/src/query_plan/optimize.rs b/apollo-federation/src/query_plan/optimize.rs new file mode 100644 index 0000000000..06de27e5f3 --- /dev/null +++ b/apollo-federation/src/query_plan/optimize.rs @@ -0,0 +1,767 @@ +//! # GraphQL subgraph query optimization. +//! +//! This module contains the logic to optimize (or "compress") a subgraph query by using fragments +//! (either reusing existing ones in the original query or generating new ones). +//! +//! ## Selection/SelectionSet intersection/minus operations +//! These set-theoretic operation methods are used to compute the optimized selection set. +//! +//! ## Collect applicable fragments at given type. +//! This is only the first filtering step. Further validation is needed to check if they can merge +//! with other fields and fragment selections. +//! +//! ## Field validation +//! `FieldsConflictMultiBranchValidator` (and `FieldsConflictValidator`) are used to check if +//! modified subgraph GraphQL queries are still valid, since adding fragments can introduce +//! conflicts. +//! +//! ## Matching fragments with selection set +//! `try_optimize_with_fragments` tries to match all applicable fragments one by one. +//! They are expanded into selection sets in order to match against given selection set. +//! Set-intersection/-minus/-containment operations are used to narrow down to fewer number of +//! fragments that can be used to optimize the selection set. If there is a single fragment that +//! covers the full selection set, then that fragment is used. Otherwise, we attempted to reduce +//! the number of fragments applied (but optimality is not guaranteed yet). + +use std::collections::HashMap; +use std::collections::HashSet; +use std::ops::Not; +use std::sync::Arc; + +use apollo_compiler::executable::Name; +use apollo_compiler::Node; + +use super::operation::CollectedFieldInSet; +use super::operation::Containment; +use super::operation::ContainmentOptions; +use super::operation::Field; +use super::operation::Fragment; +use super::operation::FragmentSpreadSelection; +use super::operation::NamedFragments; +use super::operation::NormalizeSelectionOption; +use super::operation::Selection; +use super::operation::SelectionKey; +use super::operation::SelectionSet; +use crate::error::FederationError; +use crate::schema::position::CompositeTypeDefinitionPosition; + +//============================================================================= +// Selection/SelectionSet intersection/minus operations + +impl Selection { + // PORT_NOTE: The definition of `minus` and `intersection` functions when either `self` or + // `other` has no sub-selection seems unintuitive. Why `apple.minus(orange) = None` and + // `apple.intersection(orange) = apple`? + + /// Computes the set-subtraction (self - other) and returns the result (the difference between + /// self and other). + /// If there are respective sub-selections, then we compute their diffs and add them (if not + /// empty). Otherwise, we have no diff. + fn minus(&self, other: &Selection) -> Result, FederationError> { + if let (Some(self_sub_selection), Some(other_sub_selection)) = + (self.selection_set()?, other.selection_set()?) + { + let diff = self_sub_selection.minus(other_sub_selection)?; + if !diff.is_empty() { + return self + .with_updated_selections( + self_sub_selection.type_position.clone(), + diff.into_iter().map(|(_, v)| v), + ) + .map(Some); + } + } + Ok(None) + } + + /// Computes the set-intersection of self and other + /// - If there are respective sub-selections, then we compute their intersections and add them + /// (if not empty). + /// - Otherwise, the intersection is same as `self`. + fn intersection(&self, other: &Selection) -> Result, FederationError> { + if let (Some(self_sub_selection), Some(other_sub_selection)) = + (self.selection_set()?, other.selection_set()?) + { + let common = self_sub_selection.intersection(other_sub_selection)?; + if !common.is_empty() { + return self + .with_updated_selections( + self_sub_selection.type_position.clone(), + common.into_iter().map(|(_, v)| v), + ) + .map(Some); + } + } + Ok(Some(self.clone())) + } +} + +impl SelectionSet { + /// Performs set-subtraction (self - other) and returns the result (the difference between self + /// and other). + fn minus(&self, other: &SelectionSet) -> Result { + let iter = self + .selections + .iter() + .map(|(k, v)| { + if let Some(other_v) = other.selections.get(k) { + v.minus(other_v) + } else { + Ok(Some(v.clone())) + } + }) + .collect::, _>>()? // early break in case of Err + .into_iter() + .flatten(); + Ok(SelectionSet::from_raw_selections( + self.schema.clone(), + self.type_position.clone(), + iter, + )) + } + + /// Computes the set-intersection of self and other + fn intersection(&self, other: &SelectionSet) -> Result { + if self.is_empty() { + return Ok(self.clone()); + } + if other.is_empty() { + return Ok(other.clone()); + } + + let iter = self + .selections + .iter() + .map(|(k, v)| { + if let Some(other_v) = other.selections.get(k) { + v.intersection(other_v) + } else { + Ok(None) + } + }) + .collect::, _>>()? // early break in case of Err + .into_iter() + .flatten(); + Ok(SelectionSet::from_raw_selections( + self.schema.clone(), + self.type_position.clone(), + iter, + )) + } +} + +//============================================================================= +// Filtering applicable fragments + +impl Fragment { + /// Whether this fragment may apply _directly_ at the provided type, meaning that the fragment + /// sub-selection (_without_ the fragment condition, hence the "directly") can be normalized at + /// `ty` without overly "widening" the runtime types. + /// + /// * `ty` - the type at which we're looking at applying the fragment + // + // The runtime types of the fragment condition must be at least as general as those of the + // provided `ty`. Otherwise, putting it at `ty` without its condition would "generalize" + // more than the fragment meant to (and so we'd "widen" the runtime types more than what the + // query meant to. + fn can_apply_directly_at_type( + &self, + ty: &CompositeTypeDefinitionPosition, + ) -> Result { + // Short-circuit #1: the same type => trivially true. + if self.type_condition_position == *ty { + return Ok(true); + } + + // Short-circuit #2: The type condition is not an abstract type (too restrictive). + // - It will never cover all of the runtime types of `ty` unless it's the same type, which is + // already checked by short-circuit #1. + if !self.type_condition_position.is_abstract_type() { + return Ok(false); + } + + // Short-circuit #3: The type condition is not an object (due to short-circuit #2) nor a + // union type, but the `ty` may be too general. + // - In other words, the type condition must be an interface but `ty` is a (different) + // interface or a union. + // PORT_NOTE: In JS, this check was later on the return statement (negated). But, this + // should be checked before `possible_runtime_types` check, since this is + // cheaper to execute. + // PORT_NOTE: This condition may be too restrictive (potentially a bug leading to + // suboptimal compression). If ty is a union whose members all implements the + // type condition (interface). Then, this function should've returned true. + // Thus, `!ty.is_union_type()` might be needed. + if !self.type_condition_position.is_union_type() && !ty.is_object_type() { + return Ok(false); + } + + // Check if the type condition is a superset of the provided type. + // - The fragment condition must be at least as general as the provided type. + let condition_types = self + .schema + .possible_runtime_types(self.type_condition_position.clone())?; + let ty_types = self.schema.possible_runtime_types(ty.clone())?; + Ok(condition_types.is_superset(&ty_types)) + } +} + +impl NamedFragments { + /// Returns a list of fragments that can be applied directly at the given type. + fn get_all_may_apply_directly_at_type( + &self, + ty: &CompositeTypeDefinitionPosition, + ) -> Result>, FederationError> { + self.iter() + .filter_map(|fragment| { + fragment + .can_apply_directly_at_type(ty) + .map(|can_apply| can_apply.then_some(fragment.clone())) + .transpose() + }) + .collect::, _>>() + } +} + +//============================================================================= +// Field validation + +// PORT_NOTE: Not having a validator and having a FieldsConflictValidator with empty +// `by_response_name` map has no difference in behavior. So, we could drop the `Option` from +// `Option`. However, `None` validator makes it clearer that validation is +// unnecessary. +struct FieldsConflictValidator { + by_response_name: HashMap>>>, +} + +impl FieldsConflictValidator { + fn from_selection_set(selection_set: &SelectionSet) -> Self { + Self::for_level(&selection_set.fields_in_set()) + } + + fn for_level(level: &[CollectedFieldInSet]) -> Self { + // Group `level`'s fields by the response-name/field + let mut at_level: HashMap>>> = + HashMap::new(); + for collected_field in level { + let response_name = collected_field.field().field.data().response_name(); + let at_response_name = at_level.entry(response_name).or_default(); + if let Some(ref field_selection_set) = collected_field.field().selection_set { + at_response_name + .entry(collected_field.field().field.clone()) + .or_default() + .get_or_insert_with(Default::default) + .extend(field_selection_set.fields_in_set()); + } else { + // Note that whether a `FieldSelection` has a sub-selection set or not is entirely + // determined by whether the field type is a composite type or not, so even if + // we've seen a previous version of `field` before, we know it's guaranteed to have + // no selection set here, either. So the `set` below may overwrite a previous + // entry, but it would be a `None` so no harm done. + at_response_name.insert(collected_field.field().field.clone(), None); + } + } + + // Collect validators per response-name/field + let mut by_response_name = HashMap::new(); + for (response_name, fields) in at_level { + let mut at_response_name: HashMap>> = + HashMap::new(); + for (field, collected_fields) in fields { + let validator = collected_fields + .map(|collected_fields| Arc::new(Self::for_level(&collected_fields))); + at_response_name.insert(field, validator); + } + by_response_name.insert(response_name, at_response_name); + } + Self { by_response_name } + } + + fn for_field(&self, field: &Field) -> Vec> { + let Some(by_response_name) = self.by_response_name.get(&field.data().response_name()) + else { + return Vec::new(); + }; + by_response_name.values().flatten().cloned().collect() + } + + fn has_same_response_shape( + &self, + other: &FieldsConflictValidator, + ) -> Result { + for (response_name, self_fields) in self.by_response_name.iter() { + let Some(other_fields) = other.by_response_name.get(response_name) else { + continue; + }; + + for (self_field, self_validator) in self_fields { + for (other_field, other_validator) in other_fields { + if !self_field.types_can_be_merged(other_field)? { + return Ok(false); + } + + if let Some(self_validator) = self_validator { + if let Some(other_validator) = other_validator { + if !self_validator.has_same_response_shape(other_validator)? { + return Ok(false); + } + } + } + } + } + } + Ok(true) + } + + fn do_merge_with(&self, other: &FieldsConflictValidator) -> Result { + for (response_name, self_fields) in self.by_response_name.iter() { + let Some(other_fields) = other.by_response_name.get(response_name) else { + continue; + }; + + // We're basically checking + // [FieldsInSetCanMerge](https://spec.graphql.org/draft/#FieldsInSetCanMerge()), but + // from 2 set of fields (`self_fields` and `other_fields`) of the same response that we + // know individually merge already. + for (self_field, self_validator) in self_fields { + for (other_field, other_validator) in other_fields { + if !self_field.types_can_be_merged(other_field)? { + return Ok(false); + } + + let p1 = self_field.parent_type_position(); + let p2 = other_field.parent_type_position(); + if p1 == p2 || !p1.is_object_type() || !p2.is_object_type() { + // Additional checks of `FieldsInSetCanMerge` when same parent type or one + // isn't object + if self_field.data().name() != other_field.data().name() + || self_field.data().arguments != other_field.data().arguments + { + return Ok(false); + } + if let Some(self_validator) = self_validator { + if let Some(other_validator) = other_validator { + if !self_validator.do_merge_with(other_validator)? { + return Ok(false); + } + } + } + } else { + // Otherwise, the sub-selection must pass + // [SameResponseShape](https://spec.graphql.org/draft/#SameResponseShape()). + if let Some(self_validator) = self_validator { + if let Some(other_validator) = other_validator { + if !self_validator.has_same_response_shape(other_validator)? { + return Ok(false); + } + } + } + } + } + } + } + Ok(true) + } + + fn do_merge_with_all<'a>( + &self, + mut iter: impl Iterator, + ) -> Result { + iter.try_fold(true, |acc, v| Ok(acc && self.do_merge_with(v)?)) + } +} + +struct FieldsConflictMultiBranchValidator { + validators: Vec>, + used_spread_trimmed_part_at_level: Vec>, +} + +impl FieldsConflictMultiBranchValidator { + fn new(validators: Vec>) -> Self { + Self { + validators, + used_spread_trimmed_part_at_level: Vec::new(), + } + } + + fn from_initial_validator(validator: FieldsConflictValidator) -> Self { + Self { + validators: vec![Arc::new(validator)], + used_spread_trimmed_part_at_level: Vec::new(), + } + } + + fn for_field(&self, field: &Field) -> Self { + let for_all_branches = self.validators.iter().flat_map(|v| v.for_field(field)); + Self::new(for_all_branches.collect()) + } + + // When this method is used in the context of `try_optimize_with_fragments`, we know that the + // fragment, restricted to the current parent type, matches a subset of the sub-selection. + // However, there is still one case we we cannot use it that we need to check, and this is if + // using the fragment would create a field "conflict" (in the sense of the graphQL spec + // [`FieldsInSetCanMerge`](https://spec.graphql.org/draft/#FieldsInSetCanMerge())) and thus + // create an invalid selection. To be clear, `at_type.selections` cannot create a conflict, + // since it is a subset of the target selection set and it is valid by itself. *But* there may + // be some part of the fragment that is not `at_type.selections` due to being "dead branches" + // for type `parent_type`. And while those branches _are_ "dead" as far as execution goes, the + // `FieldsInSetCanMerge` validation does not take this into account (it's 1st step says + // "including visiting fragments and inline fragments" but has no logic regarding ignoring any + // fragment that may not apply due to the intersection of runtime types between multiple + // fragment being empty). + fn check_can_reuse_fragment_and_track_it( + &mut self, + fragment_restriction: &FragmentRestrictionAtType, + ) -> Result { + // No validator means that everything in the fragment selection was part of the selection + // we're optimizing away (by using the fragment), and we know the original selection was + // ok, so nothing to check. + let Some(validator) = &fragment_restriction.validator else { + return Ok(true); // Nothing to check; Trivially ok. + }; + + if !validator.do_merge_with_all(self.validators.iter().map(Arc::as_ref))? { + return Ok(false); + } + + // We need to make sure the trimmed parts of `fragment` merges with the rest of the + // selection, but also that it merge with any of the trimmed parts of any fragment we have + // added already. + // Note: this last condition means that if 2 fragment conflict on their "trimmed" parts, + // then the choice of which is used can be based on the fragment ordering and selection + // order, which may not be optimal. This feels niche enough that we keep it simple for now, + // but we can revisit this decision if we run into real cases that justify it (but making + // it optimal would be a involved in general, as in theory you could have complex + // dependencies of fragments that conflict, even cycles, and you need to take the size of + // fragments into account to know what's best; and even then, this could even depend on + // overall usage, as it can be better to reuse a fragment that is used in other places, + // than to use one for which it's the only usage. Adding to all that the fact that conflict + // can happen in sibling branches). + if !validator.do_merge_with_all( + self.used_spread_trimmed_part_at_level + .iter() + .map(Arc::as_ref), + )? { + return Ok(false); + } + + // We're good, but track the fragment. + self.used_spread_trimmed_part_at_level + .push(validator.clone()); + Ok(true) + } +} + +//============================================================================= +// Matching fragments with selection set (`try_optimize_with_fragments`) + +/// Return type for `expanded_selection_set_at_type` method. +struct FragmentRestrictionAtType { + /// Selections that are expanded from a given fragment at a given type and then normalized. + /// - This represents the part of given type's sub-selections that are covered by the fragment. + selections: SelectionSet, + + /// A runtime validator to check the fragment selections against other fields. + /// - `None` means that there is nothing to check. + /// - See `check_can_reuse_fragment_and_track_it` for more details. + validator: Option>, +} + +impl FragmentRestrictionAtType { + fn new(selections: SelectionSet, validator: Option) -> Self { + Self { + selections, + validator: validator.map(Arc::new), + } + } + + // It's possible that while the fragment technically applies at `parent_type`, it's "rebasing" on + // `parent_type` is empty, or contains only `__typename`. For instance, suppose we have + // a union `U = A | B | C`, and then a fragment: + // ```graphql + // fragment F on U { + // ... on A { + // x + // } + // ... on B { + // y + // } + // } + // ``` + // It is then possible to apply `F` when the parent type is `C`, but this ends up selecting + // nothing at all. + // + // Using `F` in those cases is, while not 100% incorrect, at least not productive, and so we + // skip it that case. This is essentially an optimization. + fn is_useless(&self) -> bool { + match self.selections.selections.as_slice().split_first() { + None => true, + + Some((first, rest)) => rest.is_empty() && first.0.is_typename_field(), + } + } +} + +impl Fragment { + /// Computes the expanded selection set of this fragment along with its validator to check + /// against other fragments applied under the same selection set. + // PORT_NOTE: The JS version memoizes the result of this function. But, the current Rust port + // does not. + fn expanded_selection_set_at_type( + &self, + ty: &CompositeTypeDefinitionPosition, + ) -> Result { + let expanded_selection_set = self.selection_set.expand_all_fragments()?; + let normalized_selection_set = expanded_selection_set.normalize( + ty, + /*named_fragments*/ &Default::default(), + &self.schema, + NormalizeSelectionOption::NormalizeRecursively, + )?; + + if !self.type_condition_position.is_object_type() { + // When the type condition of the fragment is not an object type, the + // `FieldsInSetCanMerge` rule is more restrictive and any fields can create conflicts. + // Thus, we have to use the full validator in this case. (see + // https://github.com/graphql/graphql-spec/issues/1085 for details.) + return Ok(FragmentRestrictionAtType::new( + normalized_selection_set.clone(), + Some(FieldsConflictValidator::from_selection_set( + &expanded_selection_set, + )), + )); + } + + // Use a smaller validator for efficiency. + // Note that `trimmed` is the difference of 2 selections that may not have been normalized + // on the same parent type, so in practice, it is possible that `trimmed` contains some of + // the selections that `selectionSet` contains, but that they have been simplified in + // `selectionSet` in such a way that the `minus` call does not see it. However, it is not + // trivial to deal with this, and it is fine given that we use trimmed to create the + // validator because we know the non-trimmed parts cannot create field conflict issues so + // we're trying to build a smaller validator, but it's ok if trimmed is not as small as it + // theoretically can be. + let trimmed = expanded_selection_set.minus(&normalized_selection_set)?; + let validator = trimmed + .is_empty() + .not() + .then(|| FieldsConflictValidator::from_selection_set(&trimmed)); + Ok(FragmentRestrictionAtType::new( + normalized_selection_set.clone(), + validator, + )) + } + + /// Checks whether `self` fragment includes the other fragment (`other_fragment_name`). + // + // Note that this is slightly different from `self` "using" `other_fragment` in that this + // essentially checks if the full selection set of `other_fragment` is contained by `self`, so + // this only look at "top-level" usages. + // + // Note that this is guaranteed to return `false` if passed self's name. + // Note: This is a heuristic looking for the other named fragment used directly in the + // selection set. It may not return `true` even though the other fragment's selections + // are actually covered by self's selection set. + // PORT_NOTE: The JS version memoizes the result of this function. But, the current Rust port + // does not. + fn includes(&self, other_fragment_name: &Name) -> bool { + if self.name == *other_fragment_name { + return false; + } + + self.selection_set.selections.iter().any(|(selection_key, _)| { + matches!( + selection_key, + SelectionKey::FragmentSpread {fragment_name, directives: _} if fragment_name == other_fragment_name, + ) + }) + } +} + +/// The return type for `SelectionSet::try_optimize_with_fragments`. +#[derive(derive_more::From)] +enum SelectionSetOrFragment { + SelectionSet(SelectionSet), + Fragment(Node), +} + +impl SelectionSet { + /// Reduce the list of applicable fragments by eliminating ones that are subsumed by another. + // + // We have found the list of fragments that applies to some subset of sub-selection. In + // general, we want to now produce the selection set with spread for those fragments plus + // any selection that is not covered by any of the fragments. For instance, suppose that + // `subselection` is `{ a b c d e }` and we have found that `fragment F1 on X { a b c }` + // and `fragment F2 on X { c d }` applies, then we will generate `{ ...F1 ...F2 e }`. + // + // In that example, `c` is covered by both fragments. And this is fine in this example as + // it is worth using both fragments in general. A special case of this however is if a + // fragment is entirely included into another. That is, consider that we now have `fragment + // F1 on X { a ...F2 }` and `fragment F2 on X { b c }`. In that case, the code above would + // still match both `F1 and `F2`, but as `F1` includes `F2` already, we really want to only + // use `F1`. So in practice, we filter away any fragment spread that is known to be + // included in another one that applies. + // + // TODO: note that the logic used for this is theoretically a bit sub-optimal. That is, we + // only check if one of the fragment happens to directly include a spread for another + // fragment at top-level as in the example above. We do this because it is cheap to check + // and is likely the most common case of this kind of inclusion. But in theory, we would + // have `fragment F1 on X { a b c }` and `fragment F2 on X { b c }`, in which case `F2` is + // still included in `F1`, but we'd have to work harder to figure this out and it's unclear + // it's a good tradeoff. And while you could argue that it's on the user to define its + // fragments a bit more optimally, it's actually a tad more complex because we're looking + // at fragments in a particular context/parent type. Consider an interface `I` and: + // ```graphql + // fragment F3 on I { + // ... on X { + // a + // } + // ... on Y { + // b + // c + // } + // } + // + // fragment F4 on I { + // ... on Y { + // c + // } + // ... on Z { + // d + // } + // } + // ``` + // In that case, neither fragment include the other per-se. But what if we have + // sub-selection `{ b c }` but where parent type is `Y`. In that case, both `F3` and `F4` + // applies, and in that particular context, `F3` is fully included in `F4`. Long story + // short, we'll currently return `{ ...F3 ...F4 }` in that case, but it would be + // technically better to return only `F4`. However, this feels niche, and it might be + // costly to verify such inclusions, so not doing it for now. + fn reduce_applicable_fragments( + applicable_fragments: &mut Vec<(Node, FragmentRestrictionAtType)>, + ) { + // Note: It's not possible for two fragments to include each other. So, we don't need to + // worry about inclusion cycles. + let included_fragments: HashSet = applicable_fragments + .iter() + .filter(|(fragment, _)| { + applicable_fragments + .iter() + .any(|(other_fragment, _)| other_fragment.includes(&fragment.name)) + }) + .map(|(fragment, _)| fragment.name.clone()) + .collect(); + + applicable_fragments.retain(|(fragment, _)| !included_fragments.contains(&fragment.name)); + } + + /// Try to optimize the selection set by re-using existing fragments. + /// Returns either + /// - a new selection set partially optimized by re-using given `fragments`, or + /// - a single fragment that covers the full selection set. + // PORT_NOTE: Moved from `Selection` class in JS code to SelectionSet struct in Rust. + // PORT_NOTE: `parent_type` argument seems always to be the same as `self.type_position`. + fn try_optimize_with_fragments( + &self, + parent_type: &CompositeTypeDefinitionPosition, + fragments: &NamedFragments, + validator: &mut FieldsConflictMultiBranchValidator, + can_use_full_matching_fragment: impl Fn(&Fragment) -> bool, + ) -> Result { + // We limit to fragments whose selection could be applied "directly" at `parent_type`, + // meaning without taking the fragment condition into account. The idea being that if the + // fragment condition would be needed inside `parent_type`, then that condition will not + // have been "normalized away" and so we want for this very call to be called on the + // fragment whose type _is_ the fragment condition (at which point, this + // `can_apply_directly_at_type` method will apply. Also note that this is because we have + // this restriction that calling `expanded_selection_set_at_type` is ok. + let candidates = fragments.get_all_may_apply_directly_at_type(parent_type)?; + if candidates.is_empty() { + return Ok(self.clone().into()); // Not optimizable + } + + // First, we check which of the candidates do apply inside the selection set, if any. If we + // find a candidate that applies to the whole selection set, then we stop and only return + // that one candidate. Otherwise, we cumulate in `applicable_fragments` the list of fragments + // that applies to a subset. + let mut applicable_fragments = Vec::new(); + for candidate in candidates { + let at_type = candidate.expanded_selection_set_at_type(parent_type)?; + if at_type.is_useless() { + continue; + } + if !validator.check_can_reuse_fragment_and_track_it(&at_type)? { + // We cannot use it at all, so no point in adding to `applicable_fragments`. + continue; + } + + // As we check inclusion, we ignore the case where the fragment queries __typename + // but the `self` does not. The rational is that querying `__typename` + // unnecessarily is mostly harmless (it always works and it's super cheap) so we + // don't want to not use a fragment just to save querying a `__typename` in a few + // cases. But the underlying context of why this matters is that the query planner + // always requests __typename for abstract type, and will do so in fragments too, + // but we can have a field that _does_ return an abstract type within a fragment, + // but that _does not_ end up returning an abstract type when applied in a "more + // specific" context (think a fragment on an interface I1 where a inside field + // returns another interface I2, but applied in the context of a implementation + // type of I1 where that particular field returns an implementation of I2 rather + // than I2 directly; we would have added __typename to the fragment (because it's + // all interfaces), but the selection itself, which only deals with object type, + // may not have __typename requested; using the fragment might still be a good + // idea, and querying __typename needlessly is a very small price to pay for that). + let res = self.containment( + &at_type.selections, + ContainmentOptions { + ignore_missing_typename: true, + }, + ); + if matches!(res, Containment::NotContained) { + continue; // Not eligible; Skip it. + } + if matches!(res, Containment::Equal) && can_use_full_matching_fragment(&candidate) { + // Special case: Found a fragment that covers the full selection set. + return Ok(candidate.into()); + } + // Note that if a fragment applies to only a subset of the sub-selections, then we + // really only can use it if that fragment is defined _without_ directives. + if !candidate.directives.is_empty() { + continue; // Not eligible as a partial selection; Skip it. + } + applicable_fragments.push((candidate, at_type)); + } + + if applicable_fragments.is_empty() { + return Ok(self.clone().into()); // Not optimizable + } + + // Narrow down the list of applicable fragments by removing those that are included in + // another. + Self::reduce_applicable_fragments(&mut applicable_fragments); + + // Build a new optimized selection set. + let mut not_covered_so_far = self.clone(); + let mut optimized = SelectionSet::empty(self.schema.clone(), self.type_position.clone()); + for (fragment, at_type) in applicable_fragments { + let not_covered = self.minus(&at_type.selections)?; + not_covered_so_far = not_covered_so_far.intersection(¬_covered)?; + + // PORT_NOTE: The JS version uses `parent_type` as the "sourceType", which may be + // different from `fragment.type_condition_position`. But, Rust version does + // not have "sourceType" field for `FragmentSpreadSelection`. + let fragment_selection = FragmentSpreadSelection::from_fragment( + &fragment, + /*directives*/ &Default::default(), + ); + Arc::make_mut(&mut optimized.selections).insert(fragment_selection.into()); + } + + Arc::make_mut(&mut optimized.selections).extend_ref(¬_covered_so_far.selections); + Ok(SelectionSet::make_selection_set( + &self.schema, + parent_type, + optimized.selections.values().map(std::iter::once), + fragments, + )? + .into()) + } +} diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index 8697dd118c..ae5085c9ab 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -349,6 +349,7 @@ impl QueryPlanner { requires: Default::default(), input_rewrites: Default::default(), output_rewrites: Default::default(), + context_rewrites: Default::default(), }; return Ok(QueryPlan::new(node, statistics)); diff --git a/apollo-federation/tests/query_plan/build_query_plan_support.rs b/apollo-federation/tests/query_plan/build_query_plan_support.rs index 14b1d71c7e..ea39785ba4 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_support.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_support.rs @@ -25,19 +25,22 @@ const IMPLICIT_LINK_DIRECTIVE: &str = r#"@link(url: "https://specs.apollo.dev/fe /// This can all be remove when composition is implemented in Rust. macro_rules! planner { ( - $( config = $config: expr, )? + config = $config: expr, $( $subgraph_name: tt: $subgraph_schema: expr),+ $(,)? ) => {{ - #[allow(unused_mut)] - let mut config = Default::default(); - $( config = $config )? $crate::query_plan::build_query_plan_support::api_schema_and_planner( insta::_function_name!(), - config, + $config, &[ $( (subgraph_name!($subgraph_name), $subgraph_schema) ),+ ], ) }}; + ( + $( $subgraph_name: tt: $subgraph_schema: expr),+ + $(,)? + ) => { + planner!(config = Default::default(), $( $subgraph_name: $subgraph_schema),+) + }; } macro_rules! subgraph_name { diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests.rs b/apollo-federation/tests/query_plan/build_query_plan_tests.rs index 4c8bf1f13b..6203cadebb 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests.rs @@ -32,6 +32,7 @@ fn some_name() { */ mod fetch_operation_names; +mod named_fragments_preservation; mod provides; mod requires; mod shareable_root_fields; @@ -153,8 +154,6 @@ fn pick_keys_that_minimize_fetches() { /// (more precisely, this force the query planner to _consider_ type explosion; the generated /// query plan still ends up not type-exploding in practice since as it's not necessary). #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure fn field_covariance_and_type_explosion() { let planner = planner!( Subgraph1: r#" @@ -195,23 +194,228 @@ fn field_covariance_and_type_explosion() { } "#, @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + dummy { + field { + __typename + ... on Object { + field { + __typename + } + } + } + } + } + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn handles_non_intersecting_fragment_conditions() { + let planner = planner!( + Subgraph1: r#" + interface Fruit { + edible: Boolean! + } + + type Banana implements Fruit { + edible: Boolean! + inBunch: Boolean! + } + + type Apple implements Fruit { + edible: Boolean! + hasStem: Boolean! + } + + type Query { + fruit: Fruit! + } + "#, + ); + assert_plan!( + &planner, + r#" + fragment OrangeYouGladIDidntSayBanana on Fruit { + ... on Banana { + inBunch + } + ... on Apple { + hasStem + } + } + + query Fruitiness { + fruit { + ... on Apple { + ...OrangeYouGladIDidntSayBanana + } + } + } + "#, + @r#" QueryPlan { Fetch(service: "Subgraph1") { { - dummy { + fruit { + __typename + ... on Apple { + hasStem + } + } + } + }, + } + "# + ); +} + +#[test] +#[should_panic(expected = "snapshot assertion")] +// TODO: investigate this failure +fn avoids_unnecessary_fetches() { + // This test is a reduced example demonstrating a previous issue with the computation of query plans cost. + // The general idea is that "Subgraph 3" has a declaration that is kind of useless (it declares entity A + // that only provides it's own key, so there is never a good reason to use it), but the query planner + // doesn't know that and will "test" plans including fetch to that subgraphs in its exhaustive search + // of all options. In theory, the query plan costing mechanism should eliminate such plans in favor of + // plans not having this inefficient, but an issue in the plan cost computation led to such inefficient + // to have the same cost as the more efficient one and to be picked (just because it was the first computed). + // This test ensures this costing bug is fixed. + + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "idT") { + idT: ID! + a: A + } + + type A @key(fields: "idA2") { + idA2: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "idT") { + idT: ID! + u: U + } + + type U @key(fields: "idU") { + idU: ID! + } + "#, + Subgraph3: r#" + type A @key(fields: "idA1") { + idA1: ID! + } + "#, + Subgraph4: r#" + type A @key(fields: "idA1") @key(fields: "idA2") { + idA1: ID! + idA2: ID! + } + "#, + Subgraph5: r#" + type U @key(fields: "idU") { + idU: ID! + v: Int + } + "#, + ); + + assert_plan!( + &planner, + r#" + { + t { + u { + v + } + a { + idA1 + } + } + } + "#, + @r#" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { __typename - field { + idT + a { __typename - ... on Object { - field { + idA2 + } + } + } + }, + Parallel { + Sequence { + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + idT + } + } => + { + ... on T { + u { + __typename + idU + } + } + } + }, + }, + Flatten(path: "t.u") { + Fetch(service: "Subgraph5") { + { + ... on U { __typename + idU + } + } => + { + ... on U { + v } } + }, + }, + }, + Flatten(path: "t.a") { + Fetch(service: "Subgraph4") { + { + ... on A { + __typename + idA2 + } + } => + { + ... on A { + idA1 + } } - } - } + }, + }, }, - } - "### + }, + } + "# ); } diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_preservation.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_preservation.rs new file mode 100644 index 0000000000..231609b845 --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_preservation.rs @@ -0,0 +1,1317 @@ +use apollo_federation::query_plan::query_planner::QueryPlannerConfig; + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_works_with_nested_fragments_1() { + let planner = planner!( + Subgraph1: r#" + type Query { + a: Anything + } + + union Anything = A1 | A2 | A3 + + interface Foo { + foo: String + child: Foo + child2: Foo + } + + type A1 implements Foo { + foo: String + child: Foo + child2: Foo + } + + type A2 implements Foo { + foo: String + child: Foo + child2: Foo + } + + type A3 implements Foo { + foo: String + child: Foo + child2: Foo + } + "#, + ); + assert_plan!( + &planner, + r#" + query { + a { + ... on A1 { + ...FooSelect + } + ... on A2 { + ...FooSelect + } + ... on A3 { + ...FooSelect + } + } + } + + fragment FooSelect on Foo { + __typename + foo + child { + ...FooChildSelect + } + child2 { + ...FooChildSelect + } + } + + fragment FooChildSelect on Foo { + __typename + foo + child { + child { + child { + foo + } + } + } + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + a { + __typename + ... on A1 { + ...FooSelect + } + ... on A2 { + ...FooSelect + } + ... on A3 { + ...FooSelect + } + } + } + + fragment FooChildSelect on Foo { + __typename + foo + child { + __typename + child { + __typename + child { + __typename + foo + } + } + } + } + + fragment FooSelect on Foo { + __typename + foo + child { + ...FooChildSelect + } + child2 { + ...FooChildSelect + } + } + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_avoid_fragments_usable_only_once() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v1: V + } + + type V @shareable { + a: Int + b: Int + c: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: V + v3: V + } + + type V @shareable { + a: Int + b: Int + c: Int + } + "#, + ); + + // We use a fragment which does save some on the original query, but as each + // field gets to a different subgraph, the fragment would only be used one + // on each sub-fetch and we make sure the fragment is not used in that case. + assert_plan!( + &planner, + r#" + query { + t { + v1 { + ...OnV + } + v2 { + ...OnV + } + } + } + + fragment OnV on V { + a + b + c + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + v1 { + a + b + c + } + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 { + a + b + c + } + } + } + }, + }, + }, + } + "### + ); + + // But double-check that if we query 2 fields from the same subgraph, then + // the fragment gets used now. + assert_plan!( + &planner, + r#" + query { + t { + v2 { + ...OnV + } + v3 { + ...OnV + } + } + } + + fragment OnV on V { + a + b + c + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 { + ...OnV + } + v3 { + ...OnV + } + } + } + + fragment OnV on V { + a + b + c + } + }, + }, + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure + +fn respects_query_planner_option_reuse_query_fragments_true() { + respects_query_planner_option_reuse_query_fragments(true) +} +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure + +fn respects_query_planner_option_reuse_query_fragments_false() { + respects_query_planner_option_reuse_query_fragments(false) +} + +fn respects_query_planner_option_reuse_query_fragments(reuse_query_fragments: bool) { + let planner = planner!( + config = QueryPlannerConfig {reuse_query_fragments, ..Default::default()}, + Subgraph1: r#" + type Query { + t: T + } + + type T { + a1: A + a2: A + } + + type A { + x: Int + y: Int + } + "#, + ); + let query = r#" + query { + t { + a1 { + ...Selection + } + a2 { + ...Selection + } + } + } + + fragment Selection on A { + x + y + } + "#; + if reuse_query_fragments { + assert_plan!( + &planner, + query, + @r#" + QueryPlan { + Fetch(service: "Subgraph1") { + { + t { + a1 { + ...Selection + } + a2 { + ...Selection + } + } + } + + fragment Selection on A { + x + y + } + }, + } + "# + ); + } else { + assert_plan!( + &planner, + query, + @r#" + QueryPlan { + Fetch(service: "Subgraph1") { + { + t { + a1 { + x + y + } + a2 { + x + y + } + } + } + }, + } + "# + ); + } +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + a: V + b: V + } + + type V { + v1: Int + v2: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t { + ...OnT + } + } + + fragment OnT on T { + a { + ...OnV + } + b { + ...OnV + } + } + + fragment OnV on V { + v1 + v2 + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + t { + a { + ...OnV + } + b { + ...OnV + } + } + } + + fragment OnV on V { + v1 + v2 + } + }, + } + "### + ); +} + +#[test] +#[should_panic( + expected = "Error: variable `$if` of type `Boolean` cannot be used for argument `if` of type `Boolean!`" +)] +// TODO: investigate this failure +fn it_preserves_directives_when_fragment_not_used() { + // (because used only once) + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + a: Int + b: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + query test($if: Boolean) { + t { + id + ...OnT @include(if: $if) + } + } + + fragment OnT on T { + a + b + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + t { + id + ... on T @include(if: $if) { + a + b + } + } + } + }, + } + "### + ); +} + +#[test] +#[should_panic( + expected = "variable `$test1` of type `Boolean` cannot be used for argument `if` of type `Boolean!`" +)] +// TODO: investigate this failure +fn it_preserves_directives_when_fragment_is_reused() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + a: Int + b: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + query test($test1: Boolean, $test2: Boolean) { + t { + id + ...OnT @include(if: $test1) + ...OnT @include(if: $test2) + } + } + + fragment OnT on T { + a + b + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + t { + id + ...OnT @include(if: $test1) + ...OnT @include(if: $test2) + } + } + + fragment OnT on T { + a + b + } + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "Interface type \"I\" has no field \"b\"")] +// TODO: investigate this failure +fn it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgaph() { + // Slightly artificial example for simplicity, but this highlight the problem. + // In that example, the only queried subgraph is the first one (there is in fact + // no way to ever reach the 2nd one), so the plan should mostly simply forward + // the query to the 1st subgraph, but a subtlety is that the named fragment used + // in the query is *not* valid for Subgraph1, because it queries `b` on `I`, but + // there is no `I.b` in Subgraph1. + // So including the named fragment in the fetch would be erroneous: the subgraph + // server would reject it when validating the query, and we must make sure it + // is not reused. + let planner = planner!( + Subgraph1: r#" + type Query { + i1: I + i2: I + } + + interface I { + a: Int + } + + type T implements I { + a: Int + b: Int + } + "#, + Subgraph2: r#" + interface I { + a: Int + b: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + query { + i1 { + ... on T { + ...Frag + } + } + i2 { + ... on T { + ...Frag + } + } + } + + fragment Frag on I { + b + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + i1 { + __typename + ... on T { + b + } + } + i2 { + __typename + ... on T { + b + } + } + } + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs() { + // This test is designed such that type `Outer` implements the interface `I` in `Subgraph1` + // but not in `Subgraph2`, yet `I` exists in `Subgraph2` (but only `Inner` implements it + // there). Further, the operations we test have a fragment on I (`IFrag` below) that is + // used "in the context of `Outer`" (at the top-level of fragment `OuterFrag`). + // + // What this all means is that `IFrag` can be rebased in `Subgraph2` "as is" because `I` + // exists there with all its fields, but as we rebase `OuterFrag` on `Subgraph2`, we + // cannot use `...IFrag` inside it (at the top-level), because `I` and `Outer` do + // no intersect in `Subgraph2` and this would be an invalid selection. + // + // Previous versions of the code were not handling this case and were error out by + // creating the invalid selection (#2721), and this test ensures this is fixed. + let planner = planner!( + Subgraph1: r#" + type V @shareable { + x: Int + } + + interface I { + v: V + } + + type Outer implements I @key(fields: "id") { + id: ID! + v: V + } + "#, + Subgraph2: r#" + type Query { + outer1: Outer + outer2: Outer + } + + type V @shareable { + x: Int + } + + interface I { + v: V + w: Int + } + + type Inner implements I { + v: V + w: Int + } + + type Outer @key(fields: "id") { + id: ID! + inner: Inner + w: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } + + fragment OuterFrag on Outer { + ...IFrag + inner { + ...IFrag + } + } + + fragment IFrag on I { + v { + x + } + } + "#, + @r#" + QueryPlan { + Sequence { + Fetch(service: "Subgraph2") { + { + outer1 { + __typename + ...OuterFrag + id + } + outer2 { + __typename + ...OuterFrag + id + } + } + + fragment OuterFrag on Outer { + inner { + v { + x + } + } + } + }, + Parallel { + Flatten(path: "outer1") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v { + x + } + } + } + }, + }, + Flatten(path: "outer2") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v { + x + } + } + } + }, + }, + }, + }, + } + "# + ); + + // We very slighly modify the operation to add an artificial indirection within `IFrag`. + // This does not really change the query, and should result in the same plan, but + // ensure the code handle correctly such indirection. + assert_plan!( + &planner, + r#" + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } + + fragment OuterFrag on Outer { + ...IFrag + inner { + ...IFrag + } + } + + fragment IFrag on I { + ...IFragDelegate + } + + fragment IFragDelegate on I { + v { + x + } + } + "#, + @r#" + QueryPlan { + Sequence { + Fetch(service: "Subgraph2") { + { + outer1 { + __typename + ...OuterFrag + id + } + outer2 { + __typename + ...OuterFrag + id + } + } + + fragment OuterFrag on Outer { + inner { + v { + x + } + } + } + }, + Parallel { + Flatten(path: "outer1") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v { + x + } + } + } + }, + }, + Flatten(path: "outer2") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v { + x + } + } + } + }, + }, + }, + }, + } + "# + ); + + // The previous cases tests the cases where nothing in the `...IFrag` spread at the + // top-level of `OuterFrag` applied at all: it all gets eliminated in the plan. But + // in the schema of `Subgraph2`, while `Outer` does not implement `I` (and does not + // have `v` in particular), it does contains field `w` that `I` also have, so we + // add that field to `IFrag` and make sure we still correctly query that field. + + assert_plan!( + &planner, + r#" + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } + + fragment OuterFrag on Outer { + ...IFrag + inner { + ...IFrag + } + } + + fragment IFrag on I { + v { + x + } + w + } + "#, + @r#" + QueryPlan { + Sequence { + Fetch(service: "Subgraph2") { + { + outer1 { + __typename + ...OuterFrag + id + } + outer2 { + __typename + ...OuterFrag + id + } + } + + fragment OuterFrag on Outer { + w + inner { + v { + x + } + w + } + } + }, + Parallel { + Flatten(path: "outer1") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v { + x + } + } + } + }, + }, + Flatten(path: "outer2") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v { + x + } + } + } + }, + }, + }, + }, + } + "# + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs() { + // This test is similar to the subtyping case (it tests the same problems), but test the case + // of unions instead of interfaces. + let planner = planner!( + Subgraph1: r#" + type V @shareable { + x: Int + } + + union U = Outer + + type Outer @key(fields: "id") { + id: ID! + v: Int + } + "#, + Subgraph2: r#" + type Query { + outer1: Outer + outer2: Outer + } + + union U = Inner + + type Inner { + v: Int + w: Int + } + + type Outer @key(fields: "id") { + id: ID! + inner: Inner + w: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } + + fragment OuterFrag on Outer { + ...UFrag + inner { + ...UFrag + } + } + + fragment UFrag on U { + ... on Outer { + v + } + ... on Inner { + v + } + } + "#, + @r#" + QueryPlan { + Sequence { + Fetch(service: "Subgraph2") { + { + outer1 { + __typename + ...OuterFrag + id + } + outer2 { + __typename + ...OuterFrag + id + } + } + + fragment OuterFrag on Outer { + inner { + v + } + } + }, + Parallel { + Flatten(path: "outer1") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v + } + } + }, + }, + Flatten(path: "outer2") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v + } + } + }, + }, + }, + }, + } + "# + ); + + // We very slighly modify the operation to add an artificial indirection within `IFrag`. + // This does not really change the query, and should result in the same plan, but + // ensure the code handle correctly such indirection. + assert_plan!( + &planner, + r#" + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } + + fragment OuterFrag on Outer { + ...UFrag + inner { + ...UFrag + } + } + + fragment UFrag on U { + ...UFragDelegate + } + + fragment UFragDelegate on U { + ... on Outer { + v + } + ... on Inner { + v + } + } + "#, + @r#" + QueryPlan { + Sequence { + Fetch(service: "Subgraph2") { + { + outer1 { + __typename + ...OuterFrag + id + } + outer2 { + __typename + ...OuterFrag + id + } + } + + fragment OuterFrag on Outer { + inner { + v + } + } + }, + Parallel { + Flatten(path: "outer1") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v + } + } + }, + }, + Flatten(path: "outer2") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v + } + } + }, + }, + }, + }, + } + "# + ); + + // The previous cases tests the cases where nothing in the `...IFrag` spread at the + // top-level of `OuterFrag` applied at all: it all gets eliminated in the plan. But + // in the schema of `Subgraph2`, while `Outer` does not implement `I` (and does not + // have `v` in particular), it does contains field `w` that `I` also have, so we + // add that field to `IFrag` and make sure we still correctly query that field. + assert_plan!( + &planner, + r#" + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } + + fragment OuterFrag on Outer { + ...UFrag + inner { + ...UFrag + } + } + + fragment UFrag on U { + ... on Outer { + v + w + } + ... on Inner { + v + } + } + "#, + @r#" + QueryPlan { + Sequence { + Fetch(service: "Subgraph2") { + { + outer1 { + __typename + ...OuterFrag + id + } + outer2 { + __typename + ...OuterFrag + id + } + } + + fragment OuterFrag on Outer { + w + inner { + v + } + } + }, + Parallel { + Flatten(path: "outer1") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v + } + } + }, + }, + Flatten(path: "outer2") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v + } + } + }, + }, + }, + }, + } + "# + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/provides.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/provides.rs index 23ba713f10..79cc960d0e 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/provides.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/provides.rs @@ -302,49 +302,58 @@ fn it_works_on_unions() { // This is our sanity check: we first query _without_ the provides // to make sure we _do_ need to go the the second subgraph. @r###" - QueryPlan { - Sequence { - Fetch(service: "Subgraph1") { + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + noProvides { + ... on T1 { + __typename + id + } + ... on T2 { + __typename + id + a + } + } + } + }, + Parallel { + Flatten(path: "noProvides") { + Fetch(service: "Subgraph2") { { - noProvides { + ... on T2 { __typename - ... on T1 { - __typename - id - } - ... on T2 { - __typename - id - a - } + id + } + } => + { + ... on T2 { + b } } }, - Flatten(path: "noProvides") { - Fetch(service: "Subgraph2") { - { - ... on T1 { - __typename - id - } - ... on T2 { - __typename - id - } - } => - { - ... on T1 { - a - } - ... on T2 { - b - } + }, + Flatten(path: "noProvides") { + Fetch(service: "Subgraph2") { + { + ... on T1 { + __typename + id } - }, + } => + { + ... on T1 { + a + } + } }, }, - } - "### + }, + }, + } + "### ); // Ensuring that querying only `a` can be done with subgraph1 only when provided. @@ -363,22 +372,21 @@ fn it_works_on_unions() { } "#, @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - withProvidesForT1 { - __typename - ... on T1 { - a - } - ... on T2 { - a - } - } + QueryPlan { + Fetch(service: "Subgraph1") { + { + withProvidesForT1 { + ... on T1 { + a } - }, + ... on T2 { + a + } + } } - "### + }, + } + "### ); // But ensure that querying `b` still goes to subgraph2 if only a is provided. @@ -398,41 +406,40 @@ fn it_works_on_unions() { } "#, @r###" - QueryPlan { - Sequence { - Fetch(service: "Subgraph1") { - { - withProvidesForT1 { - __typename - ... on T1 { - a - } - ... on T2 { - __typename - id - a - } - } + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + withProvidesForT1 { + ... on T1 { + a } - }, - Flatten(path: "withProvidesForT1") { - Fetch(service: "Subgraph2") { - { - ... on T2 { - __typename - id - } - } => - { - ... on T2 { - b - } - } - }, - }, + ... on T2 { + __typename + id + a + } + } + } + }, + Flatten(path: "withProvidesForT1") { + Fetch(service: "Subgraph2") { + { + ... on T2 { + __typename + id + } + } => + { + ... on T2 { + b + } + } }, - } - "### + }, + }, + } + "### ); // Lastly, if both are provided, ensures we only hit subgraph1. diff --git a/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql b/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql new file mode 100644 index 0000000000..2a19639079 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql @@ -0,0 +1,82 @@ +# Composed from subgraphs with hash: 18e2cb5dae85e435398e8667f6866178c8022706 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type A + @join__type(graph: SUBGRAPH1, key: "idA2") + @join__type(graph: SUBGRAPH3, key: "idA1") + @join__type(graph: SUBGRAPH4, key: "idA1") + @join__type(graph: SUBGRAPH4, key: "idA2") +{ + idA2: ID! @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH4) + idA1: ID! @join__field(graph: SUBGRAPH3) @join__field(graph: SUBGRAPH4) +} + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") + SUBGRAPH3 @join__graph(name: "Subgraph3", url: "none") + SUBGRAPH4 @join__graph(name: "Subgraph4", url: "none") + SUBGRAPH5 @join__graph(name: "Subgraph5", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @join__type(graph: SUBGRAPH3) + @join__type(graph: SUBGRAPH4) + @join__type(graph: SUBGRAPH5) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "idT") + @join__type(graph: SUBGRAPH2, key: "idT") +{ + idT: ID! + a: A @join__field(graph: SUBGRAPH1) + u: U @join__field(graph: SUBGRAPH2) +} + +type U + @join__type(graph: SUBGRAPH2, key: "idU") + @join__type(graph: SUBGRAPH5, key: "idU") +{ + idU: ID! + v: Int @join__field(graph: SUBGRAPH5) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql new file mode 100644 index 0000000000..0ad0811a99 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql @@ -0,0 +1,69 @@ +# Composed from subgraphs with hash: 555913ed43f0ae0b3eae434a8b46b930fecd9e09 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type Apple implements Fruit + @join__implements(graph: SUBGRAPH1, interface: "Fruit") + @join__type(graph: SUBGRAPH1) +{ + edible: Boolean! + hasStem: Boolean! +} + +type Banana implements Fruit + @join__implements(graph: SUBGRAPH1, interface: "Fruit") + @join__type(graph: SUBGRAPH1) +{ + edible: Boolean! + inBunch: Boolean! +} + +interface Fruit + @join__type(graph: SUBGRAPH1) +{ + edible: Boolean! +} + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + fruit: Fruit! +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql b/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql new file mode 100644 index 0000000000..809c44d072 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql @@ -0,0 +1,68 @@ +# Composed from subgraphs with hash: 61f7aebc1284fd8dc840de895884ee5b7fd836c2 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: V @join__field(graph: SUBGRAPH1) + v2: V @join__field(graph: SUBGRAPH2) + v3: V @join__field(graph: SUBGRAPH2) +} + +type V + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + a: Int + b: Int + c: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgaph.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgaph.graphql new file mode 100644 index 0000000000..7230a3c168 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgaph.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 7216df3d57d6ea85890cb4557bc1400d2a32faf0 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +interface I + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + a: Int + b: Int @join__field(graph: SUBGRAPH2) +} + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + i1: I @join__field(graph: SUBGRAPH1) + i2: I @join__field(graph: SUBGRAPH1) +} + +type T implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1) +{ + a: Int + b: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql new file mode 100644 index 0000000000..836e43cc0a --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql @@ -0,0 +1,84 @@ +# Composed from subgraphs with hash: 85ca2a0ec950344b1f685478ac0ea4f36e5b7692 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +interface I + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + v: V + w: Int @join__field(graph: SUBGRAPH2) +} + +type Inner implements I + @join__implements(graph: SUBGRAPH2, interface: "I") + @join__type(graph: SUBGRAPH2) +{ + v: V + w: Int +} + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Outer implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v: V @join__field(graph: SUBGRAPH1) + inner: Inner @join__field(graph: SUBGRAPH2) + w: Int @join__field(graph: SUBGRAPH2) +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + outer1: Outer @join__field(graph: SUBGRAPH2) + outer2: Outer @join__field(graph: SUBGRAPH2) +} + +type V + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + x: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql new file mode 100644 index 0000000000..af1d65d24e --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql @@ -0,0 +1,80 @@ +# Composed from subgraphs with hash: 532639290329813c32101df1b46a23c760db68fc +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type Inner + @join__type(graph: SUBGRAPH2) +{ + v: Int + w: Int +} + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Outer + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v: Int @join__field(graph: SUBGRAPH1) + inner: Inner @join__field(graph: SUBGRAPH2) + w: Int @join__field(graph: SUBGRAPH2) +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + outer1: Outer @join__field(graph: SUBGRAPH2) + outer2: Outer @join__field(graph: SUBGRAPH2) +} + +union U + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @join__unionMember(graph: SUBGRAPH1, member: "Outer") + @join__unionMember(graph: SUBGRAPH2, member: "Inner") + = Outer | Inner + +type V + @join__type(graph: SUBGRAPH1) +{ + x: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql new file mode 100644 index 0000000000..d6ca67b84f --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql @@ -0,0 +1,55 @@ +# Composed from subgraphs with hash: b6ebc4ffa76637f33269672e1a49739b3b7091a8 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + a: Int + b: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql new file mode 100644 index 0000000000..d6ca67b84f --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql @@ -0,0 +1,55 @@ +# Composed from subgraphs with hash: b6ebc4ffa76637f33269672e1a49739b3b7091a8 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + a: Int + b: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql new file mode 100644 index 0000000000..47f05c483a --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql @@ -0,0 +1,89 @@ +# Composed from subgraphs with hash: e0dfa4222558f28e0ecb3e26077458471e69267b +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type A1 implements Foo + @join__implements(graph: SUBGRAPH1, interface: "Foo") + @join__type(graph: SUBGRAPH1) +{ + foo: String + child: Foo + child2: Foo +} + +type A2 implements Foo + @join__implements(graph: SUBGRAPH1, interface: "Foo") + @join__type(graph: SUBGRAPH1) +{ + foo: String + child: Foo + child2: Foo +} + +type A3 implements Foo + @join__implements(graph: SUBGRAPH1, interface: "Foo") + @join__type(graph: SUBGRAPH1) +{ + foo: String + child: Foo + child2: Foo +} + +union Anything + @join__type(graph: SUBGRAPH1) + @join__unionMember(graph: SUBGRAPH1, member: "A1") + @join__unionMember(graph: SUBGRAPH1, member: "A2") + @join__unionMember(graph: SUBGRAPH1, member: "A3") + = A1 | A2 | A3 + +interface Foo + @join__type(graph: SUBGRAPH1) +{ + foo: String + child: Foo + child2: Foo +} + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + a: Anything +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql new file mode 100644 index 0000000000..64e2fd6e3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: 425c9d41052b9208347cddf5826dfcaed779900a +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + a: V + b: V +} + +type V + @join__type(graph: SUBGRAPH1) +{ + v1: Int + v2: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments.graphql new file mode 100644 index 0000000000..8831cd4888 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments.graphql @@ -0,0 +1,61 @@ +# Composed from subgraphs with hash: 4b0a2b41b9cbccb8bde234dc0285c3372e220fa1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type A + @join__type(graph: SUBGRAPH1) +{ + x: Int + y: Int +} + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1) +{ + a1: A + a2: A +} diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml index 62e409a105..6bd5ca1fd5 100644 --- a/apollo-router-benchmarks/Cargo.toml +++ b/apollo-router-benchmarks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-benchmarks" -version = "1.47.0" +version = "1.48.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml index 353837020b..289c06fc2e 100644 --- a/apollo-router-scaffold/Cargo.toml +++ b/apollo-router-scaffold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-scaffold" -version = "1.47.0" +version = "1.48.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" @@ -9,10 +9,12 @@ publish = false [dependencies] anyhow = "1.0.80" clap = { version = "4.5.1", features = ["derive"] } -cargo-scaffold = { version = "0.11.0", default-features = false } +cargo-scaffold = { version = "0.14.0", default-features = false } regex = "1" str_inflector = "0.12.0" toml = "0.8.10" [dev-dependencies] tempfile = "3.10.0" copy_dir = "0.1.3" +dircmp = "0.2.0" +similar = "2.5.0" diff --git a/apollo-router-scaffold/scaffold-test/.cargo/config b/apollo-router-scaffold/scaffold-test/.cargo/config new file mode 100644 index 0000000000..24a9882b48 --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/.cargo/config @@ -0,0 +1,3 @@ +[alias] +xtask = "run --package xtask --" +router = "run --package xtask -- router" diff --git a/apollo-router-scaffold/scaffold-test/.dockerignore b/apollo-router-scaffold/scaffold-test/.dockerignore new file mode 100644 index 0000000000..c2c4a5aa95 --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/.dockerignore @@ -0,0 +1 @@ +target/** \ No newline at end of file diff --git a/apollo-router-scaffold/scaffold-test/.gitignore b/apollo-router-scaffold/scaffold-test/.gitignore new file mode 100644 index 0000000000..bba7b53950 --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/.gitignore @@ -0,0 +1,2 @@ +/target/ +/.idea/ diff --git a/apollo-router-scaffold/scaffold-test/Cargo.toml b/apollo-router-scaffold/scaffold-test/Cargo.toml new file mode 100644 index 0000000000..21bee82592 --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/Cargo.toml @@ -0,0 +1,26 @@ +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[package] +name = "apollo-router-scaffold-test" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "apollo-router-scaffold-test" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.58" +apollo-router = { path ="../../apollo-router" } +async-trait = "0.1.52" +futures = "0.3.21" +schemars = "0.8.10" +serde = "1.0.149" +serde_json = "1.0.79" +tokio = { version = "1.17.0", features = ["full"] } +tower = { version = "0.4.12", features = ["full"] } +tracing = "0.1.37" + +# this makes build scripts and proc macros faster to compile +[profile.dev.build-override] +strip = "debuginfo" +incremental = false diff --git a/apollo-router-scaffold/scaffold-test/Dockerfile b/apollo-router-scaffold/scaffold-test/Dockerfile new file mode 100644 index 0000000000..28e91cc9a6 --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/Dockerfile @@ -0,0 +1,54 @@ +# Use the rust build image from docker as our base +# renovate-automation: rustc version +FROM rust:1.76.0 as build + +# Set our working directory for the build +WORKDIR /usr/src/router + +# Update our build image and install required packages +RUN apt-get update +RUN apt-get -y install \ + npm \ + protobuf-compiler + +# Add rustfmt since build requires it +RUN rustup component add rustfmt + +# Copy the router source to our build environment +COPY . . + +# Build and install the custom binary +RUN cargo build --release + +# Make directories for config and schema +RUN mkdir -p /dist/config && \ + mkdir /dist/schema && \ + mv target/release/router /dist + +# Copy configuration for docker image +COPY router.yaml /dist/config.yaml + +FROM debian:bullseye-slim + +RUN apt-get update +RUN apt-get -y install \ + ca-certificates + +# Set labels for our image +LABEL org.opencontainers.image.authors="Apollo Graph, Inc. https://github.com/apollographql/router" +LABEL org.opencontainers.image.source="https://github.com/apollographql/router" + +# Copy in the required files from our build image +COPY --from=build --chown=root:root /dist /dist + +WORKDIR /dist + +ENV APOLLO_ROUTER_CONFIG_PATH="/dist/config.yaml" + +# Make sure we can run the router +RUN chmod 755 /dist/router + +USER router + +# Default executable is the router +ENTRYPOINT ["/dist/router"] diff --git a/apollo-router-scaffold/scaffold-test/README.md b/apollo-router-scaffold/scaffold-test/README.md new file mode 100644 index 0000000000..98ec70eb24 --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/README.md @@ -0,0 +1,120 @@ +# Apollo Router project + +This generated project is set up to create a custom Apollo Router binary that may include plugins that you have written. + +> Note: The Apollo Router is made available under the Elastic License v2.0 (ELv2). +> Read [our licensing page](https://www.apollographql.com/docs/resources/elastic-license-v2-faq/) for more details. + +# Compile the router + +To create a debug build use the following command. +```bash +cargo build +``` +Your debug binary is now located in `target/debug/router` + +For production, you will want to create a release build. +```bash +cargo build --release +``` +Your release binary is now located in `target/release/router` + +# Run the Apollo Router + +1. Download the example schema + + ```bash + curl -sSL https://supergraph.demo.starstuff.dev/ > supergraph-schema.graphql + ``` + +2. Run the Apollo Router + + During development it is convenient to use `cargo run` to run the Apollo Router as it will + ```bash + cargo run -- --hot-reload --config router.yaml --supergraph supergraph-schema.graphql + ``` + +> If you are using managed federation you can set APOLLO_KEY and APOLLO_GRAPH_REF environment variables instead of specifying the supergraph as a file. + +# Create a plugin + +1. From within your project directory scaffold a new plugin + ```bash + cargo router plugin create hello_world + ``` +2. Select the type of plugin you want to scaffold: + ```bash + Select a plugin template: + > "basic" + "auth" + "tracing" + ``` + + The different templates are: + * basic - a barebones plugin. + * auth - a basic authentication plugin that could make an external call. + * tracing - a plugin that adds a custom span and a log message. + + Choose `basic`. + +4. Add the plugin to the `router.yaml` + ```yaml + plugins: + starstuff.hello_world: + message: "Starting my plugin" + ``` + +5. Run the Apollo Router and see your plugin start up + ```bash + cargo run -- --hot-reload --config router.yaml --supergraph supergraph-schema.graphql + ``` + + In your output you should see something like: + ```bash + 2022-05-21T09:16:33.160288Z INFO router::plugins::hello_world: Starting my plugin + ``` + +# Remove a plugin + +1. From within your project run the following command. It makes a best effort to remove the plugin, but your mileage may vary. + ```bash + cargo router plugin remove hello_world + ``` + +# Docker + +You can use the provided Dockerfile to build a release container. + +Make sure your router is configured to listen to `0.0.0.0` so you can query it from outside the container: + +```yml + supergraph: + listen: 0.0.0.0:4000 +``` + +Use your `APOLLO_KEY` and `APOLLO_GRAPH_REF` environment variables to run the router in managed federation. + + ```bash + docker build -t my_custom_router . + docker run -e APOLLO_KEY="your apollo key" -e APOLLO_GRAPH_REF="your apollo graph ref" -p 4000:4000 my_custom_router + ``` + +Otherwise add a `COPY` step to the Dockerfile, and edit the entrypoint: + +```Dockerfile +# Copy configuration for docker image +COPY router.yaml /dist/config.yaml +# Copy supergraph for docker image +COPY my_supergraph.graphql /dist/supergraph.graphql + +# [...] and change the entrypoint + +# Default executable is the router +ENTRYPOINT ["/dist/router", "-s", "/dist/supergraph.graphql"] +``` + +You can now build and run your custom router: + ```bash + docker build -t my_custom_router . + docker run -p 4000:4000 my_custom_router + ``` diff --git a/apollo-router-scaffold/scaffold-test/router.yaml b/apollo-router-scaffold/scaffold-test/router.yaml new file mode 100644 index 0000000000..8411f80a8b --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/router.yaml @@ -0,0 +1,5 @@ +# uncomment this section if you plan to use the Dockerfile +# supergraph: +# listen: 0.0.0.0:4000 +plugins: + # Add plugin configuration here diff --git a/apollo-router-scaffold/scaffold-test/src/main.rs b/apollo-router-scaffold/scaffold-test/src/main.rs new file mode 100644 index 0000000000..ca6699afe9 --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/src/main.rs @@ -0,0 +1,7 @@ +mod plugins; + +use anyhow::Result; + +fn main() -> Result<()> { + apollo_router::main() +} diff --git a/apollo-router-scaffold/scaffold-test/src/plugins/auth.rs b/apollo-router-scaffold/scaffold-test/src/plugins/auth.rs new file mode 100644 index 0000000000..b0da3b45c7 --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/src/plugins/auth.rs @@ -0,0 +1,99 @@ +use std::ops::ControlFlow; + +use apollo_router::layers::ServiceBuilderExt; +use apollo_router::plugin::Plugin; +use apollo_router::plugin::PluginInit; +use apollo_router::register_plugin; +use apollo_router::services::supergraph; +use schemars::JsonSchema; +use serde::Deserialize; +use tower::BoxError; +use tower::ServiceBuilder; +use tower::ServiceExt; + +#[derive(Debug)] +struct Auth { + #[allow(dead_code)] + configuration: Conf, +} + +#[derive(Debug, Default, Deserialize, JsonSchema)] +struct Conf { + // Put your plugin configuration here. It will automatically be deserialized from JSON. + // Always put some sort of config here, even if it is just a bool to say that the plugin is enabled, + // otherwise the yaml to enable the plugin will be confusing. + message: String, +} +// This plugin is a skeleton for doing authentication that requires a remote call. +#[async_trait::async_trait] +impl Plugin for Auth { + type Config = Conf; + + async fn new(init: PluginInit) -> Result { + tracing::info!("{}", init.config.message); + Ok(Auth { + configuration: init.config, + }) + } + + fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService { + ServiceBuilder::new() + .oneshot_checkpoint_async(|request: supergraph::Request| async { + // Do some async call here to auth, and decide if to continue or not. + Ok(ControlFlow::Continue(request)) + }) + .service(service) + .boxed() + } +} + +// This macro allows us to use it in our plugin registry! +// register_plugin takes a group name, and a plugin name. +register_plugin!("acme", "auth", Auth); + +#[cfg(test)] +mod tests { + use apollo_router::graphql; + use apollo_router::services::supergraph; + use apollo_router::TestHarness; + use tower::BoxError; + use tower::ServiceExt; + + #[tokio::test] + async fn basic_test() -> Result<(), BoxError> { + let test_harness = TestHarness::builder() + .configuration_json(serde_json::json!({ + "plugins": { + "acme.auth": { + "message" : "Starting my plugin" + } + } + })) + .unwrap() + .build_router() + .await + .unwrap(); + let request = supergraph::Request::canned_builder().build().unwrap(); + let mut streamed_response = test_harness.oneshot(request.try_into()?).await?; + + let first_response: graphql::Response = serde_json::from_slice( + streamed_response + .next_response() + .await + .expect("couldn't get primary response")? + .to_vec() + .as_slice(), + ) + .unwrap(); + + assert!(first_response.data.is_some()); + + println!("first response: {:?}", first_response); + let next = streamed_response.next_response().await; + println!("next response: {:?}", next); + + // You could keep calling .next_response() until it yields None if you're expexting more parts. + assert!(next.is_none()); + Ok(()) + } +} diff --git a/apollo-router-scaffold/scaffold-test/src/plugins/basic.rs b/apollo-router-scaffold/scaffold-test/src/plugins/basic.rs new file mode 100644 index 0000000000..4f83a15401 --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/src/plugins/basic.rs @@ -0,0 +1,123 @@ +use apollo_router::plugin::Plugin; +use apollo_router::plugin::PluginInit; +use apollo_router::register_plugin; +use apollo_router::services::execution; +use apollo_router::services::router; +use apollo_router::services::subgraph; +use apollo_router::services::supergraph; +use schemars::JsonSchema; +use serde::Deserialize; +use tower::BoxError; + +#[derive(Debug)] +struct Basic { + #[allow(dead_code)] + configuration: Conf, +} + +#[derive(Debug, Default, Deserialize, JsonSchema)] +struct Conf { + // Put your plugin configuration here. It will automatically be deserialized from JSON. + // Always put some sort of config here, even if it is just a bool to say that the plugin is enabled, + // otherwise the yaml to enable the plugin will be confusing. + message: String, +} +// This is a bare bones plugin that can be duplicated when creating your own. +#[async_trait::async_trait] +impl Plugin for Basic { + type Config = Conf; + + async fn new(init: PluginInit) -> Result { + tracing::info!("{}", init.config.message); + Ok(Basic { + configuration: init.config, + }) + } + + // Delete this function if you are not customizing it. + fn router_service(&self, service: router::BoxService) -> router::BoxService { + // Always use service builder to compose your plugins. + // It provides off the shelf building blocks for your plugin. + // + // ServiceBuilder::new() + // .service(service) + // .boxed() + + // Returning the original service means that we didn't add any extra functionality at this point in the lifecycle. + service + } + + // Delete this function if you are not customizing it. + fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService { + // Always use service builder to compose your plugins. + // It provides off the shelf building blocks for your plugin. + // + // ServiceBuilder::new() + // .service(service) + // .boxed() + + // Returning the original service means that we didn't add any extra functionality for at this point in the lifecycle. + service + } + + // Delete this function if you are not customizing it. + fn execution_service(&self, service: execution::BoxService) -> execution::BoxService { + service + } + + // Delete this function if you are not customizing it. + fn subgraph_service(&self, _name: &str, service: subgraph::BoxService) -> subgraph::BoxService { + service + } +} + +// This macro allows us to use it in our plugin registry! +// register_plugin takes a group name, and a plugin name. +register_plugin!("acme", "basic", Basic); + +#[cfg(test)] +mod tests { + use apollo_router::graphql; + use apollo_router::services::supergraph; + use apollo_router::TestHarness; + use tower::BoxError; + use tower::ServiceExt; + + #[tokio::test] + async fn basic_test() -> Result<(), BoxError> { + let test_harness = TestHarness::builder() + .configuration_json(serde_json::json!({ + "plugins": { + "acme.basic": { + "message" : "Starting my plugin" + } + } + })) + .unwrap() + .build_router() + .await + .unwrap(); + let request = supergraph::Request::canned_builder().build().unwrap(); + let mut streamed_response = test_harness.oneshot(request.try_into()?).await?; + + let first_response: graphql::Response = serde_json::from_slice( + streamed_response + .next_response() + .await + .expect("couldn't get primary response")? + .to_vec() + .as_slice(), + ) + .unwrap(); + + assert!(first_response.data.is_some()); + + println!("first response: {:?}", first_response); + let next = streamed_response.next_response().await; + println!("next response: {:?}", next); + + // You could keep calling .next_response() until it yields None if you're expexting more parts. + assert!(next.is_none()); + Ok(()) + } +} diff --git a/apollo-router-scaffold/scaffold-test/src/plugins/mod.rs b/apollo-router-scaffold/scaffold-test/src/plugins/mod.rs new file mode 100644 index 0000000000..738d9a3f7e --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/src/plugins/mod.rs @@ -0,0 +1,3 @@ +mod auth; +mod basic; +mod tracing; diff --git a/apollo-router-scaffold/scaffold-test/src/plugins/tracing.rs b/apollo-router-scaffold/scaffold-test/src/plugins/tracing.rs new file mode 100644 index 0000000000..e62ba30df3 --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/src/plugins/tracing.rs @@ -0,0 +1,103 @@ +use apollo_router::layers::ServiceBuilderExt; +use apollo_router::plugin::Plugin; +use apollo_router::plugin::PluginInit; +use apollo_router::register_plugin; +use apollo_router::services::supergraph; +use schemars::JsonSchema; +use serde::Deserialize; +use tower::BoxError; +use tower::ServiceBuilder; +use tower::ServiceExt; + +#[derive(Debug)] +struct Tracing { + #[allow(dead_code)] + configuration: Conf, +} + +#[derive(Debug, Default, Deserialize, JsonSchema)] +struct Conf { + // Put your plugin configuration here. It will automatically be deserialized from JSON. + // Always put some sort of config here, even if it is just a bool to say that the plugin is enabled, + // otherwise the yaml to enable the plugin will be confusing. + message: String, +} +// This plugin adds a span and an error to the logs. +#[async_trait::async_trait] +impl Plugin for Tracing { + type Config = Conf; + + async fn new(init: PluginInit) -> Result { + tracing::info!("{}", init.config.message); + Ok(Tracing { + configuration: init.config, + }) + } + + fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService { + ServiceBuilder::new() + .instrument(|_request| { + // Optionally take information from the request and insert it into the span as attributes + // See https://docs.rs/tracing/latest/tracing/ for more information + tracing::info_span!("my_custom_span") + }) + .map_request(|request| { + // Add a log message, this will appear within the context of the current span + tracing::error!("error detected"); + request + }) + .service(service) + .boxed() + } +} + +// This macro allows us to use it in our plugin registry! +// register_plugin takes a group name, and a plugin name. +register_plugin!("acme", "tracing", Tracing); + +#[cfg(test)] +mod tests { + use apollo_router::graphql; + use apollo_router::services::supergraph; + use apollo_router::TestHarness; + use tower::BoxError; + use tower::ServiceExt; + + #[tokio::test] + async fn basic_test() -> Result<(), BoxError> { + let test_harness = TestHarness::builder() + .configuration_json(serde_json::json!({ + "plugins": { + "acme.tracing": { + "message" : "Starting my plugin" + } + } + })) + .unwrap() + .build_router() + .await + .unwrap(); + let request = supergraph::Request::canned_builder().build().unwrap(); + let mut streamed_response = test_harness.oneshot(request.try_into()?).await?; + + let first_response: graphql::Response = serde_json::from_slice( + streamed_response + .next_response() + .await + .expect("couldn't get primary response")? + .to_vec() + .as_slice(), + ) + .unwrap(); + + assert!(first_response.data.is_some()); + + println!("first response: {:?}", first_response); + let next = streamed_response.next_response().await; + println!("next response: {:?}", next); + + // You could keep calling .next_response() until it yields None if you're expexting more parts. + assert!(next.is_none()); + Ok(()) + } +} diff --git a/apollo-router-scaffold/scaffold-test/xtask/Cargo.toml b/apollo-router-scaffold/scaffold-test/xtask/Cargo.toml new file mode 100644 index 0000000000..c96b9d0e98 --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/xtask/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xtask" +edition = "2021" +publish = false +version = "0.1.0" + +[dependencies] +# This dependency should stay in line with your router version + +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.46.0" } +anyhow = "1.0.58" +clap = "4.0.32" diff --git a/apollo-router-scaffold/scaffold-test/xtask/src/main.rs b/apollo-router-scaffold/scaffold-test/xtask/src/main.rs new file mode 100644 index 0000000000..211ded5255 --- /dev/null +++ b/apollo-router-scaffold/scaffold-test/xtask/src/main.rs @@ -0,0 +1,38 @@ +use anyhow::Result; +use apollo_router_scaffold::RouterAction; +use clap::Parser; +use clap::Subcommand; + +#[derive(Parser, Debug)] +struct Args { + #[clap(subcommand)] + action: Action, +} + +impl Args { + fn execute(&self) -> Result<()> { + self.action.execute() + } +} + +#[derive(Subcommand, Debug)] +enum Action { + /// Forward to router action + Router { + #[clap(subcommand)] + action: RouterAction, + }, +} + +impl Action { + fn execute(&self) -> Result<()> { + match self { + Action::Router { action } => action.execute(), + } + } +} + +fn main() -> Result<()> { + let args = Args::parse(); + args.execute() +} diff --git a/apollo-router-scaffold/src/lib.rs b/apollo-router-scaffold/src/lib.rs index 1731e67172..dbc76bc877 100644 --- a/apollo-router-scaffold/src/lib.rs +++ b/apollo-router-scaffold/src/lib.rs @@ -29,17 +29,14 @@ mod test { use std::path::Path; use std::path::PathBuf; use std::path::MAIN_SEPARATOR; - use std::process::Command; - use anyhow::bail; use anyhow::Result; use cargo_scaffold::Opts; use cargo_scaffold::ScaffoldDescription; + use dircmp::Comparison; use inflector::Inflector; - use tempfile::TempDir; - - #[test] - fn the_next_test_takes_a_while_to_pass_do_not_worry() {} + use similar::ChangeTag; + use similar::TextDiff; #[test] // this test takes a while, I hope the above test name @@ -55,17 +52,13 @@ mod test { .prefix("router_scaffold") .tempdir() .unwrap(); - std::fs::copy( - repo_root.join("rust-toolchain.toml"), - temp_dir.path().join("rust-toolchain.toml"), - ) - .unwrap(); + let temp_dir_path = temp_dir.path(); let current_dir = env::current_dir().unwrap(); // Scaffold the main project let opts = Opts::builder(PathBuf::from("templates").join("base")) .project_name("temp") - .target_dir(temp_dir.path()) + .target_dir(temp_dir_path) .force(true); ScaffoldDescription::new(opts) .unwrap() @@ -87,39 +80,59 @@ mod test { ), )])) .unwrap(); - std::fs::copy( - repo_root.join("Cargo.lock"), - temp_dir.path().join("Cargo.lock"), - ) - .unwrap(); - let main = temp_dir.path().join("src").join("main.rs"); - std::fs::write( - &main, - format!( - "#![deny(warnings)]\n{}", - std::fs::read_to_string(&main).unwrap() - ), - ) - .unwrap(); - let _ = test_build_with_backup_folder(&temp_dir, &target_dir); // Scaffold one of each type of plugin - scaffold_plugin(¤t_dir, &temp_dir, "basic").unwrap(); - scaffold_plugin(¤t_dir, &temp_dir, "auth").unwrap(); - scaffold_plugin(¤t_dir, &temp_dir, "tracing").unwrap(); + scaffold_plugin(¤t_dir, temp_dir_path, "basic").unwrap(); + scaffold_plugin(¤t_dir, temp_dir_path, "auth").unwrap(); + scaffold_plugin(¤t_dir, temp_dir_path, "tracing").unwrap(); std::fs::write( temp_dir.path().join("src").join("plugins").join("mod.rs"), "mod auth;\nmod basic;\nmod tracing;\n", ) .unwrap(); - test_build_with_backup_folder(&temp_dir, &target_dir).unwrap() + #[cfg(target_os = "windows")] + let left = ".\\scaffold-test\\"; + #[cfg(not(target_os = "windows"))] + let left = "./scaffold-test/"; + + let cmp = Comparison::default(); + let diff = cmp + .compare(left, temp_dir_path.to_str().unwrap()) + .expect("should compare"); + + let mut found = false; + if !diff.is_empty() { + println!("generated scaffolding project has changed:\n{:#?}", diff); + for file in diff.changed { + println!("file: {file:?}"); + let file = PathBuf::from(file.to_str().unwrap().strip_prefix(left).unwrap()); + + // we do not check the Cargo.toml files because they have differences due to import paths and workspace usage + if file == PathBuf::from("Cargo.toml") || file == PathBuf::from("xtask/Cargo.toml") + { + println!("skipping {}", file.to_str().unwrap()); + continue; + } + // we are not dealing with windows line endings + if file == PathBuf::from("src\\plugins\\mod.rs") { + println!("skipping {}", file.to_str().unwrap()); + continue; + } + + found = true; + diff_file(&PathBuf::from("./scaffold-test"), temp_dir_path, &file); + } + if found { + panic!(); + } + } } - fn scaffold_plugin(current_dir: &Path, dir: &TempDir, plugin_type: &str) -> Result<()> { + fn scaffold_plugin(current_dir: &Path, dir_path: &Path, plugin_type: &str) -> Result<()> { let opts = Opts::builder(PathBuf::from("templates").join("plugin")) .project_name(plugin_type) - .target_dir(dir.path()) + .target_dir(dir_path) .append(true); ScaffoldDescription::new(opts)?.scaffold_with_parameters(BTreeMap::from([ ( @@ -159,37 +172,33 @@ mod test { Ok(()) } - fn test_build_with_backup_folder(temp_dir: &TempDir, target_dir: &Path) -> Result<()> { - test_build(temp_dir, target_dir).map_err(|e| { - let mut output_dir = std::env::temp_dir(); - output_dir.push("test_scaffold_output"); - - // best effort to prepare the output directory - let _ = std::fs::remove_dir_all(&output_dir); - copy_dir::copy_dir(temp_dir, &output_dir) - .expect("couldn't copy test_scaffold_output directory"); - anyhow::anyhow!( - "scaffold test failed: {e}\nYou can find the scaffolded project at '{}'", - output_dir.display() - ) - }) - } + fn diff_file(left_folder: &Path, right_folder: &Path, file: &Path) { + println!("file changed: {}\n", file.to_str().unwrap()); + let left = std::fs::read_to_string(left_folder.join(file)).unwrap(); + let right = std::fs::read_to_string(right_folder.join(file)).unwrap(); - fn test_build(dir: &TempDir, target_dir: &Path) -> Result<()> { - let output = Command::new("cargo") - .args(["test"]) - .env("CARGO_TARGET_DIR", target_dir) - .current_dir(dir) - .output()?; - if !output.status.success() { - eprintln!("failed to build scaffolded project"); - eprintln!("{}", String::from_utf8(output.stdout)?); - eprintln!("{}", String::from_utf8(output.stderr)?); - bail!( - "build failed with exit code {}", - output.status.code().unwrap_or_default() + let diff = TextDiff::from_lines(&left, &right); + + for change in diff.iter_all_changes() { + let sign = match change.tag() { + ChangeTag::Delete => "-", + ChangeTag::Insert => "+", + ChangeTag::Equal => " ", + }; + print!( + "{} {}|\t{}{}", + change + .old_index() + .map(|s| s.to_string()) + .unwrap_or("-".to_string()), + change + .new_index() + .map(|s| s.to_string()) + .unwrap_or("-".to_string()), + sign, + change ); } - Ok(()) + println!("\n\n"); } } diff --git a/apollo-router-scaffold/templates/base/Cargo.toml b/apollo-router-scaffold/templates/base/Cargo.toml index 521a9198d7..054b77f5bf 100644 --- a/apollo-router-scaffold/templates/base/Cargo.toml +++ b/apollo-router-scaffold/templates/base/Cargo.toml @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" } apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} # Note if you update these dependencies then also update xtask/Cargo.toml -apollo-router = "1.47.0" +apollo-router = "1.48.0" {{/if}} {{/if}} async-trait = "0.1.52" diff --git a/apollo-router-scaffold/templates/base/Dockerfile b/apollo-router-scaffold/templates/base/Dockerfile index fc3620751b..28e91cc9a6 100644 --- a/apollo-router-scaffold/templates/base/Dockerfile +++ b/apollo-router-scaffold/templates/base/Dockerfile @@ -45,5 +45,10 @@ WORKDIR /dist ENV APOLLO_ROUTER_CONFIG_PATH="/dist/config.yaml" +# Make sure we can run the router +RUN chmod 755 /dist/router + +USER router + # Default executable is the router ENTRYPOINT ["/dist/router"] diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.toml index 39354e9139..b8225d2ea8 100644 --- a/apollo-router-scaffold/templates/base/xtask/Cargo.toml +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.toml @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" } {{#if branch}} apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.47.0" } +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.48.0" } {{/if}} {{/if}} anyhow = "1.0.58" diff --git a/apollo-router-scaffold/templates/plugin/src/plugins/{{snake_name}}.rs b/apollo-router-scaffold/templates/plugin/src/plugins/{{snake_name}}.rs index 49a11b8d91..2208f6eb3f 100644 --- a/apollo-router-scaffold/templates/plugin/src/plugins/{{snake_name}}.rs +++ b/apollo-router-scaffold/templates/plugin/src/plugins/{{snake_name}}.rs @@ -1,26 +1,33 @@ +{{#if type_auth}} +use std::ops::ControlFlow; + +use apollo_router::layers::ServiceBuilderExt; +{{/if}} +{{#if type_tracing}} +use apollo_router::layers::ServiceBuilderExt; +{{/if}} use apollo_router::plugin::Plugin; use apollo_router::plugin::PluginInit; use apollo_router::register_plugin; -use apollo_router::services::supergraph; {{#if type_basic}} -use apollo_router::services::router; use apollo_router::services::execution; +{{/if}} +{{#if type_basic}} +use apollo_router::services::router; use apollo_router::services::subgraph; {{/if}} +use apollo_router::services::supergraph; +use schemars::JsonSchema; +use serde::Deserialize; +use tower::BoxError; {{#if type_auth}} -use apollo_router::layers::ServiceBuilderExt; -use std::ops::ControlFlow; -use tower::ServiceExt; use tower::ServiceBuilder; +use tower::ServiceExt; {{/if}} {{#if type_tracing}} -use apollo_router::layers::ServiceBuilderExt; -use tower::ServiceExt; use tower::ServiceBuilder; +use tower::ServiceExt; {{/if}} -use schemars::JsonSchema; -use serde::Deserialize; -use tower::BoxError; #[derive(Debug)] struct {{pascal_name}} { @@ -43,14 +50,13 @@ impl Plugin for {{pascal_name}} { async fn new(init: PluginInit) -> Result { tracing::info!("{}", init.config.message); - Ok({{pascal_name}} { configuration: init.config }) + Ok({{pascal_name}} { + configuration: init.config, + }) } // Delete this function if you are not customizing it. - fn router_service( - &self, - service: router::BoxService, - ) -> router::BoxService { + fn router_service(&self, service: router::BoxService) -> router::BoxService { // Always use service builder to compose your plugins. // It provides off the shelf building blocks for your plugin. // @@ -63,10 +69,7 @@ impl Plugin for {{pascal_name}} { } // Delete this function if you are not customizing it. - fn supergraph_service( - &self, - service: supergraph::BoxService, - ) -> supergraph::BoxService { + fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService { // Always use service builder to compose your plugins. // It provides off the shelf building blocks for your plugin. // @@ -79,19 +82,12 @@ impl Plugin for {{pascal_name}} { } // Delete this function if you are not customizing it. - fn execution_service( - &self, - service: execution::BoxService, - ) -> execution::BoxService { + fn execution_service(&self, service: execution::BoxService) -> execution::BoxService { service } // Delete this function if you are not customizing it. - fn subgraph_service( - &self, - _name: &str, - service: subgraph::BoxService, - ) -> subgraph::BoxService { + fn subgraph_service(&self, _name: &str, service: subgraph::BoxService) -> subgraph::BoxService { service } } @@ -104,21 +100,19 @@ impl Plugin for {{pascal_name}} { async fn new(init: PluginInit) -> Result { tracing::info!("{}", init.config.message); - Ok({{pascal_name}} { configuration: init.config }) + Ok({{pascal_name}} { + configuration: init.config, + }) } - fn supergraph_service( - &self, - service: supergraph::BoxService, - ) -> supergraph::BoxService { - + fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService { ServiceBuilder::new() - .oneshot_checkpoint_async(|request : supergraph::Request| async { - // Do some async call here to auth, and decide if to continue or not. - Ok(ControlFlow::Continue(request)) - }) - .service(service) - .boxed() + .oneshot_checkpoint_async(|request: supergraph::Request| async { + // Do some async call here to auth, and decide if to continue or not. + Ok(ControlFlow::Continue(request)) + }) + .service(service) + .boxed() } } {{/if}} @@ -130,27 +124,25 @@ impl Plugin for {{pascal_name}} { async fn new(init: PluginInit) -> Result { tracing::info!("{}", init.config.message); - Ok({{pascal_name}} { configuration: init.config }) + Ok({{pascal_name}} { + configuration: init.config, + }) } - fn supergraph_service( - &self, - service: supergraph::BoxService, - ) -> supergraph::BoxService { - + fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService { ServiceBuilder::new() - .instrument(|_request| { - // Optionally take information from the request and insert it into the span as attributes - // See https://docs.rs/tracing/latest/tracing/ for more information - tracing::info_span!("my_custom_span") - }) - .map_request(|request| { - // Add a log message, this will appear within the context of the current span - tracing::error!("error detected"); - request - }) - .service(service) - .boxed() + .instrument(|_request| { + // Optionally take information from the request and insert it into the span as attributes + // See https://docs.rs/tracing/latest/tracing/ for more information + tracing::info_span!("my_custom_span") + }) + .map_request(|request| { + // Add a log message, this will appear within the context of the current span + tracing::error!("error detected"); + request + }) + .service(service) + .boxed() } } {{/if}} @@ -161,9 +153,9 @@ register_plugin!("{{project_name}}", "{{snake_name}}", {{pascal_name}}); #[cfg(test)] mod tests { - use apollo_router::TestHarness; - use apollo_router::services::supergraph; use apollo_router::graphql; + use apollo_router::services::supergraph; + use apollo_router::TestHarness; use tower::BoxError; use tower::ServiceExt; @@ -184,11 +176,15 @@ mod tests { let request = supergraph::Request::canned_builder().build().unwrap(); let mut streamed_response = test_harness.oneshot(request.try_into()?).await?; - let first_response: graphql::Response = - serde_json::from_slice(streamed_response + let first_response: graphql::Response = serde_json::from_slice( + streamed_response .next_response() .await - .expect("couldn't get primary response")?.to_vec().as_slice()).unwrap(); + .expect("couldn't get primary response")? + .to_vec() + .as_slice(), + ) + .unwrap(); assert!(first_response.data.is_some()); @@ -201,4 +197,3 @@ mod tests { Ok(()) } } - diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 02c72d6e4e..c129c3ef37 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router" -version = "1.47.0" +version = "1.48.0" authors = ["Apollo Graph, Inc. "] repository = "https://github.com/apollographql/router/" documentation = "https://docs.rs/apollo-router" @@ -68,7 +68,7 @@ askama = "0.12.1" access-json = "0.1.0" anyhow = "1.0.80" apollo-compiler.workspace = true -apollo-federation = { path = "../apollo-federation", version = "=1.47.0" } +apollo-federation = { path = "../apollo-federation", version = "=1.48.0"} arc-swap = "1.6.0" async-channel = "1.9.0" async-compression = { version = "0.4.6", features = [ @@ -82,7 +82,7 @@ axum = { version = "0.6.20", features = ["headers", "json", "original-uri"] } base64 = "0.21.7" bloomfilter = "1.0.13" buildstructor = "0.5.4" -bytes = "1.5.0" +bytes = "1.6.0" clap = { version = "4.5.1", default-features = false, features = [ "env", "derive", @@ -188,9 +188,9 @@ regex = "1.10.3" reqwest.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.5.21+v2.7.5" +router-bridge = "=0.5.25+v2.8.0" -rust-embed = "8.2.0" +rust-embed = { version = "8.2.0", features = ["include-exclude"] } rustls = "0.21.11" rustls-native-certs = "0.6.3" rustls-pemfile = "1.0.4" @@ -331,7 +331,7 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", ] } tracing-opentelemetry = "0.21.0" -tracing-test = "0.2.4" +tracing-test = "0.2.5" walkdir = "2.4.0" wiremock = "0.5.22" libtest-mimic = "0.7.2" diff --git a/apollo-router/src/axum_factory/axum_http_server_factory.rs b/apollo-router/src/axum_factory/axum_http_server_factory.rs index 2747a81ca4..39a267fe9e 100644 --- a/apollo-router/src/axum_factory/axum_http_server_factory.rs +++ b/apollo-router/src/axum_factory/axum_http_server_factory.rs @@ -799,7 +799,10 @@ mod tests { assert_eq!(mode, SpanMode::Deprecated); } - #[tokio::test] + // Perform a short wait, (100ns) which is intended to complete before the http router call. If + // it does complete first, then the http router call will be cancelled and we'll see an error + // log in our assert. + #[tokio::test(flavor = "multi_thread")] async fn request_cancel_log() { let mut http_router = crate::TestHarness::builder() .configuration_yaml(include_str!("testdata/log_on_broken_pipe.router.yaml")) @@ -811,7 +814,7 @@ mod tests { async { let _res = tokio::time::timeout( - std::time::Duration::from_micros(100), + std::time::Duration::from_nanos(100), http_router.call( http::Request::builder() .method("POST") @@ -823,15 +826,17 @@ mod tests { ), ) .await; - - tokio::time::sleep(std::time::Duration::from_millis(1000)).await; } .with_subscriber(assert_snapshot_subscriber!( tracing_core::LevelFilter::ERROR )) .await } - #[tokio::test] + + // Perform a short wait, (100ns) which is intended to complete before the http router call. If + // it does complete first, then the http router call will be cancelled and we'll not see an + // error log in our assert. + #[tokio::test(flavor = "multi_thread")] async fn request_cancel_no_log() { let mut http_router = crate::TestHarness::builder() .configuration_yaml(include_str!("testdata/no_log_on_broken_pipe.router.yaml")) @@ -843,7 +848,7 @@ mod tests { async { let _res = tokio::time::timeout( - std::time::Duration::from_micros(100), + std::time::Duration::from_nanos(100), http_router.call( http::Request::builder() .method("POST") @@ -855,8 +860,6 @@ mod tests { ), ) .await; - - tokio::time::sleep(std::time::Duration::from_millis(1000)).await; } .with_subscriber(assert_snapshot_subscriber!( tracing_core::LevelFilter::ERROR diff --git a/apollo-router/src/cache/mod.rs b/apollo-router/src/cache/mod.rs index e6d571d79d..80daa1d8a0 100644 --- a/apollo-router/src/cache/mod.rs +++ b/apollo-router/src/cache/mod.rs @@ -52,7 +52,13 @@ where Self::with_capacity(config.in_memory.limit, config.redis.clone(), caller).await } - pub(crate) async fn get(&self, key: &K) -> Entry { + /// `init_from_redis` is called with values newly deserialized from Redis cache + /// if an error is returned, the value is ignored and considered a cache miss. + pub(crate) async fn get( + &self, + key: &K, + init_from_redis: impl FnMut(&mut V) -> Result<(), String>, + ) -> Entry { // waiting on a value from the cache is a potentially long(millisecond scale) task that // can involve a network call to an external database. To reduce the waiting time, we // go through a wait map to register interest in data associated with a key. @@ -90,7 +96,7 @@ where // request other keys independently drop(locked_wait_map); - if let Some(value) = self.storage.get(key).await { + if let Some(value) = self.storage.get(key, init_from_redis).await { self.send(sender, key, value.clone()).await; return Entry { @@ -216,7 +222,7 @@ mod tests { .await .unwrap(); - let entry = cache.get(&k).await; + let entry = cache.get(&k, |_| Ok(())).await; if entry.is_first() { // potentially long and complex async task that can fail @@ -236,7 +242,7 @@ mod tests { .unwrap(); for i in 0..14 { - let entry = cache.get(&i).await; + let entry = cache.get(&i, |_| Ok(())).await; entry.insert(i).await; } @@ -264,7 +270,7 @@ mod tests { // one delegated retrieve is made let mut computations: FuturesUnordered<_> = (0..100) .map(|_| async { - let entry = cache.get(&1).await; + let entry = cache.get(&1, |_| Ok(())).await; if entry.is_first() { let value = mock.retrieve(1).await; entry.insert(value).await; diff --git a/apollo-router/src/cache/storage.rs b/apollo-router/src/cache/storage.rs index 85581bc45c..b72ad9d378 100644 --- a/apollo-router/src/cache/storage.rs +++ b/apollo-router/src/cache/storage.rs @@ -89,7 +89,13 @@ where }) } - pub(crate) async fn get(&self, key: &K) -> Option { + /// `init_from_redis` is called with values newly deserialized from Redis cache + /// if an error is returned, the value is ignored and considered a cache miss. + pub(crate) async fn get( + &self, + key: &K, + mut init_from_redis: impl FnMut(&mut V) -> Result<(), String>, + ) -> Option { let instant_memory = Instant::now(); let res = self.inner.lock().await.get(key).cloned(); @@ -124,7 +130,18 @@ where let instant_redis = Instant::now(); if let Some(redis) = self.redis.as_ref() { let inner_key = RedisKey(key.clone()); - match redis.get::(inner_key).await { + let redis_value = + redis + .get::(inner_key) + .await + .and_then(|mut v| match init_from_redis(&mut v.0) { + Ok(()) => Some(v), + Err(e) => { + tracing::error!("Invalid value from Redis cache: {e}"); + None + } + }); + match redis_value { Some(v) => { self.inner.lock().await.put(key.clone(), v.0.clone()); diff --git a/apollo-router/src/configuration/metrics.rs b/apollo-router/src/configuration/metrics.rs index ca64ba144d..58a3fb44f8 100644 --- a/apollo-router/src/configuration/metrics.rs +++ b/apollo-router/src/configuration/metrics.rs @@ -338,6 +338,8 @@ impl InstrumentData { "$..instruments.supergraph", opt.instruments.subgraph, "$..instruments.subgraph", + opt.instruments.graphql, + "$..instruments.graphql", opt.instruments.default_attribute_requirement_level, "$..instruments.default_attribute_requirement_level", opt.spans, @@ -373,8 +375,8 @@ impl InstrumentData { ); populate_config_instrument!( - apollo.router.config.experimental_demand_control, - "$.experimental_demand_control[?(@.enabled == true)]", + apollo.router.config.demand_control, + "$.preview_demand_control[?(@.enabled == true)]", opt.mode, "$.mode" ); @@ -383,12 +385,12 @@ impl InstrumentData { // The jsonpath spec doesn't include a utility for getting the keys out of an object, so we do it manually. if let Some((_, demand_control_attributes)) = self .data - .get_mut(&"apollo.router.config.experimental_demand_control".to_string()) + .get_mut(&"apollo.router.config.demand_control".to_string()) { Self::get_first_key_from_path( demand_control_attributes, "opt.strategy", - "$.experimental_demand_control[?(@.enabled == true)].strategy", + "$.preview_demand_control[?(@.enabled == true)].strategy", yaml, ); } diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@demand_control.router.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@demand_control.router.yaml.snap index 0f2821a59a..4d9ccb8ae3 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@demand_control.router.yaml.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@demand_control.router.yaml.snap @@ -2,7 +2,7 @@ source: apollo-router/src/configuration/metrics.rs expression: "&metrics.non_zero()" --- -- name: apollo.router.config.experimental_demand_control +- name: apollo.router.config.demand_control data: datapoints: - value: 1 diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@telemetry.router.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@telemetry.router.yaml.snap index 1431bd3f5a..50a0d132d5 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@telemetry.router.yaml.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@telemetry.router.yaml.snap @@ -13,6 +13,7 @@ expression: "&metrics.non_zero()" opt.events.supergraph: true opt.instruments: true opt.instruments.default_attribute_requirement_level: false + opt.instruments.graphql: true opt.instruments.router: true opt.instruments.subgraph: true opt.instruments.supergraph: true diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index d7e2bb53e1..e46d0b68a3 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -518,6 +518,7 @@ expression: "&schema" "description": "Configuration for chaos testing, trying to reproduce bugs that require uncommon conditions. You probably don’t want this in production!", "properties": { "force_reload": { + "default": null, "description": "Force a hot reload of the Router (as if the schema or configuration had changed) at a regular time interval.", "nullable": true, "type": "string" @@ -533,11 +534,13 @@ expression: "&schema" "description": "#/definitions/UriEndpoint" }, "password": { + "default": null, "description": "The optional password", "nullable": true, "type": "string" }, "username": { + "default": null, "description": "The optional username", "nullable": true, "type": "string" @@ -590,6 +593,143 @@ expression: "&schema" } ] }, + "Condition_for_GraphQLSelector": { + "oneOf": [ + { + "additionalProperties": false, + "description": "A condition to check a selection against a value.", + "properties": { + "eq": { + "items": { + "$ref": "#/definitions/SelectorOrValue_for_GraphQLSelector", + "description": "#/definitions/SelectorOrValue_for_GraphQLSelector" + }, + "maxItems": 2, + "minItems": 2, + "type": "array" + } + }, + "required": [ + "eq" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "The first selection must be greater than the second selection.", + "properties": { + "gt": { + "items": { + "$ref": "#/definitions/SelectorOrValue_for_GraphQLSelector", + "description": "#/definitions/SelectorOrValue_for_GraphQLSelector" + }, + "maxItems": 2, + "minItems": 2, + "type": "array" + } + }, + "required": [ + "gt" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "The first selection must be less than the second selection.", + "properties": { + "lt": { + "items": { + "$ref": "#/definitions/SelectorOrValue_for_GraphQLSelector", + "description": "#/definitions/SelectorOrValue_for_GraphQLSelector" + }, + "maxItems": 2, + "minItems": 2, + "type": "array" + } + }, + "required": [ + "lt" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "A condition to check a selection against a selector.", + "properties": { + "exists": { + "$ref": "#/definitions/GraphQLSelector", + "description": "#/definitions/GraphQLSelector" + } + }, + "required": [ + "exists" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "All sub-conditions must be true.", + "properties": { + "all": { + "items": { + "$ref": "#/definitions/Condition_for_GraphQLSelector", + "description": "#/definitions/Condition_for_GraphQLSelector" + }, + "type": "array" + } + }, + "required": [ + "all" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "At least one sub-conditions must be true.", + "properties": { + "any": { + "items": { + "$ref": "#/definitions/Condition_for_GraphQLSelector", + "description": "#/definitions/Condition_for_GraphQLSelector" + }, + "type": "array" + } + }, + "required": [ + "any" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "The sub-condition must not be true", + "properties": { + "not": { + "$ref": "#/definitions/Condition_for_GraphQLSelector", + "description": "#/definitions/Condition_for_GraphQLSelector" + } + }, + "required": [ + "not" + ], + "type": "object" + }, + { + "description": "Static true condition", + "enum": [ + "true" + ], + "type": "string" + }, + { + "description": "Static false condition", + "enum": [ + "false" + ], + "type": "string" + } + ] + }, "Condition_for_RouterSelector": { "oneOf": [ { @@ -1269,6 +1409,7 @@ expression: "&schema" "nullable": true }, "deduplicate_variables": { + "default": null, "description": "DEPRECATED, now always enabled: Enable variable deduplication optimization when sending requests to subgraphs (https://github.com/apollographql/router/issues/87)", "nullable": true, "type": "boolean" @@ -1367,6 +1508,7 @@ expression: "&schema" "description": "Configuration for entity caching", "properties": { "enabled": { + "default": null, "description": "activates caching for all subgraphs, unless overriden in subgraph specific configuration", "nullable": true, "type": "boolean" @@ -1536,6 +1678,7 @@ expression: "&schema" "type": "array" }, "expose_headers": { + "default": null, "description": "Which response headers should be made available to scripts running in the browser, in response to a cross-origin request.", "items": { "type": "string" @@ -1544,6 +1687,7 @@ expression: "&schema" "type": "array" }, "match_origins": { + "default": null, "description": "`Regex`es you want to match the origins against to determine if they're allowed. Defaults to an empty list. Note that `origins` will be evaluated before `match_origins`", "items": { "type": "string" @@ -1552,6 +1696,7 @@ expression: "&schema" "type": "array" }, "max_age": { + "default": null, "description": "The `Access-Control-Max-Age` header value in time units", "type": "string" }, @@ -1758,6 +1903,29 @@ expression: "&schema" } ] }, + "DefaultedStandardInstrument_for_extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "additionalProperties": false, + "properties": { + "attributes": { + "$ref": "#/definitions/extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector", + "description": "#/definitions/extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector" + } + }, + "required": [ + "attributes" + ], + "type": "object" + } + ] + }, "DemandControlConfig": { "additionalProperties": false, "description": "Demand control configuration", @@ -1906,6 +2074,7 @@ expression: "&schema" "type": "array" }, "include_messages": { + "default": null, "description": "Will include the error message in a \"message\" attribute", "nullable": true, "type": "boolean" @@ -1955,6 +2124,38 @@ expression: "&schema" } ] }, + "Event_for_GraphQLSelector": { + "oneOf": [ + { + "description": "For every supergraph response payload (including subscription events and defer events)", + "enum": [ + "event_duration" + ], + "type": "string" + }, + { + "description": "For every supergraph response payload (including subscription events and defer events)", + "enum": [ + "event_unit" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "For every supergraph response payload (including subscription events and defer events)", + "properties": { + "event_custom": { + "$ref": "#/definitions/GraphQLSelector", + "description": "#/definitions/GraphQLSelector" + } + }, + "required": [ + "event_custom" + ], + "type": "object" + } + ] + }, "Event_for_RouterAttributes_and_RouterSelector": { "description": "An event that can be logged as part of a trace. The event has an implicit `type` attribute that matches the name of the event in the yaml and a message that can be used to provide additional information.", "properties": { @@ -2288,6 +2489,131 @@ expression: "&schema" }, "type": "object" }, + "FieldName": { + "oneOf": [ + { + "description": "The GraphQL field name", + "enum": [ + "string" + ], + "type": "string" + } + ] + }, + "FieldType": { + "oneOf": [ + { + "description": "The GraphQL field name", + "enum": [ + "name" + ], + "type": "string" + }, + { + "description": "The GraphQL field type - `bool` - `number` - `scalar` - `object` - `list`", + "enum": [ + "type" + ], + "type": "string" + } + ] + }, + "Field_for_GraphQLSelector": { + "oneOf": [ + { + "enum": [ + "field_unit" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "For every field", + "properties": { + "field_custom": { + "$ref": "#/definitions/GraphQLSelector", + "description": "#/definitions/GraphQLSelector" + } + }, + "required": [ + "field_custom" + ], + "type": "object" + } + ] + }, + "Field_for_RouterSelector": { + "oneOf": [ + { + "enum": [ + "field_unit" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "For every field", + "properties": { + "field_custom": { + "$ref": "#/definitions/RouterSelector", + "description": "#/definitions/RouterSelector" + } + }, + "required": [ + "field_custom" + ], + "type": "object" + } + ] + }, + "Field_for_SubgraphSelector": { + "oneOf": [ + { + "enum": [ + "field_unit" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "For every field", + "properties": { + "field_custom": { + "$ref": "#/definitions/SubgraphSelector", + "description": "#/definitions/SubgraphSelector" + } + }, + "required": [ + "field_custom" + ], + "type": "object" + } + ] + }, + "Field_for_SupergraphSelector": { + "oneOf": [ + { + "enum": [ + "field_unit" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "For every field", + "properties": { + "field_custom": { + "$ref": "#/definitions/SupergraphSelector", + "description": "#/definitions/SupergraphSelector" + } + }, + "required": [ + "field_custom" + ], + "type": "object" + } + ] + }, "FileUploadProtocols": { "additionalProperties": false, "description": "Configuration for the various protocols supported by the file upload plugin", @@ -2454,25 +2780,170 @@ expression: "&schema" } ] }, + "GraphQLAttributes": { + "additionalProperties": false, + "properties": { + "graphql.field.name": { + "default": null, + "description": "The GraphQL field name", + "nullable": true, + "type": "boolean" + }, + "graphql.field.type": { + "default": null, + "description": "The GraphQL field type", + "nullable": true, + "type": "boolean" + }, + "graphql.list.length": { + "default": null, + "description": "If the field is a list, the length of the list", + "nullable": true, + "type": "boolean" + }, + "graphql.operation.name": { + "default": null, + "description": "The GraphQL operation name", + "nullable": true, + "type": "boolean" + }, + "graphql.type.name": { + "default": null, + "description": "The GraphQL type name", + "nullable": true, + "type": "boolean" + } + }, + "type": "object" + }, + "GraphQLInstrumentsConfig": { + "additionalProperties": false, + "properties": { + "field.execution": { + "$ref": "#/definitions/DefaultedStandardInstrument_for_extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector", + "description": "#/definitions/DefaultedStandardInstrument_for_extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector" + }, + "list.length": { + "$ref": "#/definitions/DefaultedStandardInstrument_for_extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector", + "description": "#/definitions/DefaultedStandardInstrument_for_extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector" + } + }, + "type": "object" + }, + "GraphQLSelector": { + "anyOf": [ + { + "additionalProperties": false, + "description": "If the field is a list, the length of the list", + "properties": { + "list_length": { + "$ref": "#/definitions/ListLength", + "description": "#/definitions/ListLength" + } + }, + "required": [ + "list_length" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "The GraphQL field name", + "properties": { + "field_name": { + "$ref": "#/definitions/FieldName", + "description": "#/definitions/FieldName" + } + }, + "required": [ + "field_name" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "The GraphQL field type", + "properties": { + "field_type": { + "$ref": "#/definitions/FieldType", + "description": "#/definitions/FieldType" + } + }, + "required": [ + "field_type" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "The GraphQL type name", + "properties": { + "type_name": { + "$ref": "#/definitions/TypeName", + "description": "#/definitions/TypeName" + } + }, + "required": [ + "type_name" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "default": { + "description": "Optional default value.", + "nullable": true, + "type": "string" + }, + "operation_name": { + "$ref": "#/definitions/OperationName", + "description": "#/definitions/OperationName" + } + }, + "required": [ + "operation_name" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "static": { + "$ref": "#/definitions/AttributeValue", + "description": "#/definitions/AttributeValue" + } + }, + "required": [ + "static" + ], + "type": "object" + } + ] + }, "GrpcExporter": { "additionalProperties": false, "properties": { "ca": { + "default": null, "description": "The optional certificate authority (CA) certificate to be used in TLS configuration.", "nullable": true, "type": "string" }, "cert": { + "default": null, "description": "The optional cert for tls config", "nullable": true, "type": "string" }, "domain_name": { + "default": null, "description": "The optional domain name for tls config. Note that domain name is will be defaulted to match the endpoint is not explicitly set.", "nullable": true, "type": "string" }, "key": { + "default": null, "description": "The optional private key file for TLS configuration.", "nullable": true, "type": "string" @@ -2674,6 +3145,7 @@ expression: "&schema" "type": "boolean" }, "graph_ref": { + "default": null, "description": "Graph reference This will allow you to redirect from the Apollo Router landing page back to Apollo Studio Explorer", "nullable": true, "type": "string" @@ -2852,6 +3324,26 @@ expression: "&schema" } ] }, + "InstrumentValue_for_GraphQLSelector": { + "anyOf": [ + { + "$ref": "#/definitions/Standard", + "description": "#/definitions/Standard" + }, + { + "$ref": "#/definitions/Event_for_GraphQLSelector", + "description": "#/definitions/Event_for_GraphQLSelector" + }, + { + "$ref": "#/definitions/Field_for_GraphQLSelector", + "description": "#/definitions/Field_for_GraphQLSelector" + }, + { + "$ref": "#/definitions/GraphQLSelector", + "description": "#/definitions/GraphQLSelector" + } + ] + }, "InstrumentValue_for_RouterSelector": { "anyOf": [ { @@ -2862,6 +3354,10 @@ expression: "&schema" "$ref": "#/definitions/Event_for_RouterSelector", "description": "#/definitions/Event_for_RouterSelector" }, + { + "$ref": "#/definitions/Field_for_RouterSelector", + "description": "#/definitions/Field_for_RouterSelector" + }, { "$ref": "#/definitions/RouterSelector", "description": "#/definitions/RouterSelector" @@ -2878,6 +3374,10 @@ expression: "&schema" "$ref": "#/definitions/Event_for_SubgraphSelector", "description": "#/definitions/Event_for_SubgraphSelector" }, + { + "$ref": "#/definitions/Field_for_SubgraphSelector", + "description": "#/definitions/Field_for_SubgraphSelector" + }, { "$ref": "#/definitions/SubgraphSelector", "description": "#/definitions/SubgraphSelector" @@ -2894,12 +3394,52 @@ expression: "&schema" "$ref": "#/definitions/Event_for_SupergraphSelector", "description": "#/definitions/Event_for_SupergraphSelector" }, + { + "$ref": "#/definitions/Field_for_SupergraphSelector", + "description": "#/definitions/Field_for_SupergraphSelector" + }, { "$ref": "#/definitions/SupergraphSelector", "description": "#/definitions/SupergraphSelector" } ] }, + "Instrument_for_GraphQLAttributes_and_GraphQLSelector": { + "additionalProperties": false, + "properties": { + "attributes": { + "$ref": "#/definitions/extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector", + "description": "#/definitions/extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector" + }, + "condition": { + "$ref": "#/definitions/Condition_for_GraphQLSelector", + "description": "#/definitions/Condition_for_GraphQLSelector" + }, + "description": { + "description": "The description of the instrument.", + "type": "string" + }, + "type": { + "$ref": "#/definitions/InstrumentType", + "description": "#/definitions/InstrumentType" + }, + "unit": { + "description": "The units of the instrument, e.g. \"ms\", \"bytes\", \"requests\".", + "type": "string" + }, + "value": { + "$ref": "#/definitions/InstrumentValue_for_GraphQLSelector", + "description": "#/definitions/InstrumentValue_for_GraphQLSelector" + } + }, + "required": [ + "description", + "type", + "unit", + "value" + ], + "type": "object" + }, "Instrument_for_RouterAttributes_and_RouterSelector": { "additionalProperties": false, "properties": { @@ -3034,6 +3574,10 @@ expression: "&schema" "$ref": "#/definitions/DefaultAttributeRequirementLevel", "description": "#/definitions/DefaultAttributeRequirementLevel" }, + "graphql": { + "$ref": "#/definitions/extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::GraphQLInstrumentsConfig_apollo_router::plugins::telemetry::config_new::instruments::Instrument", + "description": "#/definitions/extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::GraphQLInstrumentsConfig_apollo_router::plugins::telemetry::config_new::instruments::Instrument" + }, "router": { "$ref": "#/definitions/extendable_attribute_apollo_router::plugins::telemetry::config_new::instruments::RouterInstrumentsConfig_apollo_router::plugins::telemetry::config_new::instruments::Instrument", "description": "#/definitions/extendable_attribute_apollo_router::plugins::telemetry::config_new::instruments::RouterInstrumentsConfig_apollo_router::plugins::telemetry::config_new::instruments::Instrument" @@ -3093,6 +3637,7 @@ expression: "&schema" "additionalProperties": false, "properties": { "algorithms": { + "default": null, "description": "List of accepted algorithms. Possible values are `HS256`, `HS384`, `HS512`, `ES256`, `ES384`, `RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`, `EdDSA`", "items": { "type": "string" @@ -3143,6 +3688,7 @@ expression: "&schema" "type": "integer" }, "max_aliases": { + "default": null, "description": "If set, requests with operations with more aliases than this maximum are rejected with a HTTP 400 Bad Request response and GraphQL error with `\"extensions\": {\"code\": \"MAX_ALIASES_LIMIT\"}`", "format": "uint32", "minimum": 0.0, @@ -3150,6 +3696,7 @@ expression: "&schema" "type": "integer" }, "max_depth": { + "default": null, "description": "If set, requests with operations deeper than this maximum are rejected with a HTTP 400 Bad Request response and GraphQL error with `\"extensions\": {\"code\": \"MAX_DEPTH_LIMIT\"}`\n\nCounts depth of an operation, looking at its selection sets, including fields in fragments and inline fragments. The following example has a depth of 3.\n\n```graphql query getProduct { book { # 1 ...bookDetails } }\n\nfragment bookDetails on Book { details { # 2 ... on ProductDetailsBook { country # 3 } } } ```", "format": "uint32", "minimum": 0.0, @@ -3157,6 +3704,7 @@ expression: "&schema" "type": "integer" }, "max_height": { + "default": null, "description": "If set, requests with operations higher than this maximum are rejected with a HTTP 400 Bad Request response and GraphQL error with `\"extensions\": {\"code\": \"MAX_DEPTH_LIMIT\"}`\n\nHeight is based on simple merging of fields using the same name or alias, but only within the same selection set. For example `name` here is only counted once and the query has height 3, not 4:\n\n```graphql query { name { first } name { last } } ```\n\nThis may change in a future version of Apollo Router to do [full field merging across fragments][merging] instead.\n\n[merging]: https://spec.graphql.org/October2021/#sec-Field-Selection-Merging]", "format": "uint32", "minimum": 0.0, @@ -3164,6 +3712,7 @@ expression: "&schema" "type": "integer" }, "max_root_fields": { + "default": null, "description": "If set, requests with operations with more root fields than this maximum are rejected with a HTTP 400 Bad Request response and GraphQL error with `\"extensions\": {\"code\": \"MAX_ROOT_FIELDS_LIMIT\"}`\n\nThis limit counts only the top level fields in a selection set, including fragments and inline fragments.", "format": "uint32", "minimum": 0.0, @@ -3192,6 +3741,17 @@ expression: "&schema" }, "type": "object" }, + "ListLength": { + "oneOf": [ + { + "description": "The length of the list", + "enum": [ + "value" + ], + "type": "string" + } + ] + }, "ListenAddr": { "anyOf": [ { @@ -3241,11 +3801,13 @@ expression: "&schema" "type": "object" }, "service_name": { + "default": null, "description": "Set a service.name resource in your metrics", "nullable": true, "type": "string" }, "service_namespace": { + "default": null, "description": "Set a service.namespace attribute in your metrics", "nullable": true, "type": "string" @@ -3415,11 +3977,13 @@ expression: "&schema" "type": "object" }, "service_name": { + "default": null, "description": "Set a service.name resource in your metrics", "nullable": true, "type": "string" }, "service_namespace": { + "default": null, "description": "Set a service.namespace attribute in your metrics", "nullable": true, "type": "string" @@ -3777,6 +4341,7 @@ expression: "&schema" "type": "boolean" }, "timeout": { + "default": null, "description": "Redis request timeout (default: 2ms)", "nullable": true, "type": "string" @@ -3853,6 +4418,7 @@ expression: "&schema" "description": "#/definitions/AvailableParallelism" }, "experimental_paths_limit": { + "default": null, "description": "Before creating query plans, for each path of fields in the query we compute all the possible options to traverse that path via the subgraphs. Multiple options can arise because fields in the path can be provided by multiple subgraphs, and abstract types (i.e. unions and interfaces) returned by fields sometimes require the query planner to traverse through each constituent object type. The number of options generated in this computation can grow large if the schema or query are sufficiently complex, and that will increase the time spent planning.\n\nThis config allows specifying a per-path limit to the number of options considered. If any path's options exceeds this limit, query planning will abort and the operation will fail.\n\nThe default value is None, which specifies no limit.", "format": "uint32", "minimum": 0.0, @@ -3860,6 +4426,7 @@ expression: "&schema" "type": "integer" }, "experimental_plans_limit": { + "default": null, "description": "Sets a limit to the number of generated query plans. The planning process generates many different query plans as it explores the graph, and the list can grow large. By using this limit, we prevent that growth and still get a valid query plan, but it may not be the optimal one.\n\nThe default limit is set to 10000, but it may change in the future", "format": "uint32", "minimum": 0.0, @@ -3872,6 +4439,7 @@ expression: "&schema" "type": "boolean" }, "warmed_up_queries": { + "default": null, "description": "Warms up the cache on reloads by running the query plan over a list of the most used queries (from the in memory cache) Configures the number of queries warmed up. Defaults to 1/3 of the in memory cache", "format": "uint", "minimum": 0.0, @@ -3971,6 +4539,7 @@ expression: "&schema" "type": "boolean" }, "timeout": { + "default": null, "description": "Redis request timeout (default: 2ms)", "nullable": true, "type": "string" @@ -3981,6 +4550,7 @@ expression: "&schema" "nullable": true }, "ttl": { + "default": null, "description": "TTL for entries", "nullable": true, "type": "string" @@ -4091,6 +4661,7 @@ expression: "&schema" "type": "number" }, "ttl": { + "default": null, "description": "how long a single deposit should be considered. Must be between 1 and 60 seconds, default value is 10 seconds", "type": "string" } @@ -4113,116 +4684,139 @@ expression: "&schema" "description": "Common attributes for http server and client. See https://opentelemetry.io/docs/specs/semconv/http/http-spans/#common-attributes", "properties": { "baggage": { + "default": null, "description": "All key values from trace baggage.", "nullable": true, "type": "boolean" }, "dd.trace_id": { + "default": null, "description": "The datadog trace ID. This can be output in logs and used to correlate traces in Datadog.", "nullable": true, "type": "boolean" }, "error.type": { + "default": null, "description": "Describes a class of error the operation ended with. Examples: * timeout * name_resolution_error * 500 Requirement level: Conditionally Required: If request has ended with an error.", "nullable": true, "type": "boolean" }, "http.request.body.size": { + "default": null, "description": "The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples: * 3495 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "http.request.method": { + "default": null, "description": "HTTP request method. Examples: * GET * POST * HEAD Requirement level: Required", "nullable": true, "type": "boolean" }, "http.response.body.size": { + "default": null, "description": "The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples: * 3495 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "http.response.status_code": { + "default": null, "description": "HTTP response status code. Examples: * 200 Requirement level: Conditionally Required: If and only if one was received/sent.", "nullable": true, "type": "boolean" }, "http.route": { + "default": null, "description": "The matched route (path template in the format used by the respective server framework). Examples: * /graphql Requirement level: Conditionally Required: If and only if it’s available", "nullable": true, "type": "boolean" }, "network.local.address": { + "default": null, "description": "Local socket address. Useful in case of a multi-IP host. Examples: * 10.1.2.80 * /tmp/my.sock Requirement level: Opt-In", "nullable": true, "type": "boolean" }, "network.local.port": { + "default": null, "description": "Local socket port. Useful in case of a multi-port host. Examples: * 65123 Requirement level: Opt-In", "nullable": true, "type": "boolean" }, "network.peer.address": { + "default": null, "description": "Peer address of the network connection - IP address or Unix domain socket name. Examples: * 10.1.2.80 * /tmp/my.sock Requirement level: Recommended", "nullable": true, "type": "boolean" }, "network.peer.port": { + "default": null, "description": "Peer port number of the network connection. Examples: * 65123 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "network.protocol.name": { + "default": null, "description": "OSI application layer or non-OSI equivalent. Examples: * http * spdy Requirement level: Recommended: if not default (http).", "nullable": true, "type": "boolean" }, "network.protocol.version": { + "default": null, "description": "Version of the protocol specified in network.protocol.name. Examples: * 1.0 * 1.1 * 2 * 3 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "network.transport": { + "default": null, "description": "OSI transport layer. Examples: * tcp * udp Requirement level: Conditionally Required", "nullable": true, "type": "boolean" }, "network.type": { + "default": null, "description": "OSI network layer or non-OSI equivalent. Examples: * ipv4 * ipv6 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "server.address": { + "default": null, "description": "Name of the local HTTP server that received the request. Examples: * example.com * 10.1.2.80 * /tmp/my.sock Requirement level: Recommended", "nullable": true, "type": "boolean" }, "server.port": { + "default": null, "description": "Port of the local HTTP server that received the request. Examples: * 80 * 8080 * 443 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "trace_id": { + "default": null, "description": "The OpenTelemetry trace ID. This can be output in logs.", "nullable": true, "type": "boolean" }, "url.path": { + "default": null, "description": "The URI path component Examples: * /search Requirement level: Required", "nullable": true, "type": "boolean" }, "url.query": { + "default": null, "description": "The URI query component Examples: * q=OpenTelemetry Requirement level: Conditionally Required: If and only if one was received/sent.", "nullable": true, "type": "boolean" }, "url.scheme": { + "default": null, "description": "The URI scheme component identifying the used protocol. Examples: * http * https Requirement level: Required", "nullable": true, "type": "boolean" }, "user_agent.original": { + "default": null, "description": "Value of the HTTP User-Agent header sent by the client. Examples: * CERN-LineMode/2.15 * libwww/2.17b3 Requirement level: Recommended", "nullable": true, "type": "boolean" @@ -4492,14 +5086,15 @@ expression: "&schema" "type": "object" }, { + "description": "Deprecated, should not be used anymore, use static field instead", "type": "string" }, { "additionalProperties": false, "properties": { "static": { - "description": "A static string value", - "type": "string" + "$ref": "#/definitions/AttributeValue", + "description": "#/definitions/AttributeValue" } }, "required": [ @@ -4544,6 +5139,7 @@ expression: "&schema" "nullable": true }, "timeout": { + "default": null, "description": "Enable timeout for incoming requests", "type": "string" } @@ -4616,6 +5212,18 @@ expression: "&schema" }, "type": "object" }, + "SelectorOrValue_for_GraphQLSelector": { + "anyOf": [ + { + "$ref": "#/definitions/AttributeValue", + "description": "#/definitions/AttributeValue" + }, + { + "$ref": "#/definitions/GraphQLSelector", + "description": "#/definitions/GraphQLSelector" + } + ] + }, "SelectorOrValue_for_RouterSelector": { "anyOf": [ { @@ -4848,11 +5456,13 @@ expression: "&schema" "description": "Per subgraph configuration for entity caching", "properties": { "enabled": { + "default": null, "description": "activates caching for this subgraph, overrides the global configuration", "nullable": true, "type": "boolean" }, "private_id": { + "default": null, "description": "Context key used to separate cache sections per user", "nullable": true, "type": "string" @@ -4881,21 +5491,25 @@ expression: "&schema" "additionalProperties": false, "properties": { "subgraph.graphql.document": { + "default": null, "description": "The GraphQL document being executed. Examples: * query findBookById { bookById(id: ?) { name } } Requirement level: Recommended", "nullable": true, "type": "boolean" }, "subgraph.graphql.operation.name": { + "default": null, "description": "The name of the operation being executed. Examples: * findBookById Requirement level: Recommended", "nullable": true, "type": "boolean" }, "subgraph.graphql.operation.type": { + "default": null, "description": "The type of the operation being executed. Examples: * query * subscription * mutation Requirement level: Recommended", "nullable": true, "type": "boolean" }, "subgraph.name": { + "default": null, "description": "The name of the subgraph Examples: * products Requirement level: Required", "nullable": true, "type": "boolean" @@ -5453,14 +6067,15 @@ expression: "&schema" "type": "object" }, { + "description": "Deprecated, should not be used anymore, use static field instead", "type": "string" }, { "additionalProperties": false, "properties": { "static": { - "description": "A static string value", - "type": "string" + "$ref": "#/definitions/AttributeValue", + "description": "#/definitions/AttributeValue" } }, "required": [ @@ -5513,6 +6128,7 @@ expression: "&schema" "nullable": true }, "timeout": { + "default": null, "description": "Enable timeout for incoming requests", "type": "string" } @@ -5570,6 +6186,7 @@ expression: "&schema" "type": "boolean" }, "max_opened_subscriptions": { + "default": null, "description": "This is a limit to only have maximum X opened subscriptions at the same time. By default if it's not set there is no limit.", "format": "uint", "minimum": 0.0, @@ -5581,6 +6198,7 @@ expression: "&schema" "description": "#/definitions/SubscriptionModeConfig" }, "queue_capacity": { + "default": null, "description": "It represent the capacity of the in memory queue to know how many events we can keep in a buffer", "format": "uint", "minimum": 0.0, @@ -5626,6 +6244,7 @@ expression: "&schema" "type": "boolean" }, "experimental_reuse_query_fragments": { + "default": null, "description": "Enable reuse of query fragments Default: depends on the federation version", "nullable": true, "type": "boolean" @@ -5661,36 +6280,43 @@ expression: "&schema" "description": "Attributes for Cost", "properties": { "cost.actual": { + "default": null, "description": "The actual cost of the operation using the currently configured cost model", "nullable": true, "type": "boolean" }, "cost.delta": { + "default": null, "description": "The delta (estimated - actual) cost of the operation using the currently configured cost model", "nullable": true, "type": "boolean" }, "cost.estimated": { + "default": null, "description": "The estimated cost of the operation using the currently configured cost model", "nullable": true, "type": "boolean" }, "cost.result": { + "default": null, "description": "The cost result, this is an error code returned by the cost calculation or COST_OK", "nullable": true, "type": "boolean" }, "graphql.document": { + "default": null, "description": "The GraphQL document being executed. Examples: * query findBookById { bookById(id: ?) { name } } Requirement level: Recommended", "nullable": true, "type": "boolean" }, "graphql.operation.name": { + "default": null, "description": "The name of the operation being executed. Examples: * findBookById Requirement level: Recommended", "nullable": true, "type": "boolean" }, "graphql.operation.type": { + "default": null, "description": "The type of the operation being executed. Examples: * query * subscription * mutation Requirement level: Recommended", "nullable": true, "type": "boolean" @@ -6026,14 +6652,15 @@ expression: "&schema" "type": "object" }, { + "description": "Deprecated, should not be used anymore, use static field instead", "type": "string" }, { "additionalProperties": false, "properties": { "static": { - "description": "A static string value", - "type": "string" + "$ref": "#/definitions/AttributeValue", + "description": "#/definitions/AttributeValue" } }, "required": [ @@ -6041,6 +6668,19 @@ expression: "&schema" ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "on_graphql_error": { + "description": "Boolean set to true if the response body contains graphql error", + "type": "boolean" + } + }, + "required": [ + "on_graphql_error" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { @@ -6148,6 +6788,7 @@ expression: "&schema" "description": "Configuration options pertaining to the subgraph server component.", "properties": { "certificate_authorities": { + "default": null, "description": "list of certificate authorities in PEM format", "nullable": true, "type": "string" @@ -6241,6 +6882,13 @@ expression: "&schema" "datadog" ], "type": "string" + }, + { + "description": "Apollo Studio trace id", + "enum": [ + "apollo" + ], + "type": "string" } ] }, @@ -6336,11 +6984,13 @@ expression: "&schema" "description": "#/definitions/SamplerOption" }, "service_name": { + "default": null, "description": "The trace service name", "nullable": true, "type": "string" }, "service_namespace": { + "default": null, "description": "The trace service namespace", "nullable": true, "type": "string" @@ -6352,6 +7002,17 @@ expression: "&schema" "description": "Per subgraph configuration for entity caching", "type": "string" }, + "TypeName": { + "oneOf": [ + { + "description": "The GraphQL type name", + "enum": [ + "string" + ], + "type": "string" + } + ] + }, "UriEndpoint": { "type": "string" }, @@ -6364,6 +7025,7 @@ expression: "&schema" "description": "#/definitions/HeartbeatInterval" }, "path": { + "default": null, "description": "Path on which WebSockets are listening", "nullable": true, "type": "string" @@ -6438,116 +7100,139 @@ expression: "&schema" "description": "Common attributes for http server and client. See https://opentelemetry.io/docs/specs/semconv/http/http-spans/#common-attributes", "properties": { "baggage": { + "default": null, "description": "All key values from trace baggage.", "nullable": true, "type": "boolean" }, "dd.trace_id": { + "default": null, "description": "The datadog trace ID. This can be output in logs and used to correlate traces in Datadog.", "nullable": true, "type": "boolean" }, "error.type": { + "default": null, "description": "Describes a class of error the operation ended with. Examples: * timeout * name_resolution_error * 500 Requirement level: Conditionally Required: If request has ended with an error.", "nullable": true, "type": "boolean" }, "http.request.body.size": { + "default": null, "description": "The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples: * 3495 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "http.request.method": { + "default": null, "description": "HTTP request method. Examples: * GET * POST * HEAD Requirement level: Required", "nullable": true, "type": "boolean" }, "http.response.body.size": { + "default": null, "description": "The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples: * 3495 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "http.response.status_code": { + "default": null, "description": "HTTP response status code. Examples: * 200 Requirement level: Conditionally Required: If and only if one was received/sent.", "nullable": true, "type": "boolean" }, "http.route": { + "default": null, "description": "The matched route (path template in the format used by the respective server framework). Examples: * /graphql Requirement level: Conditionally Required: If and only if it’s available", "nullable": true, "type": "boolean" }, "network.local.address": { + "default": null, "description": "Local socket address. Useful in case of a multi-IP host. Examples: * 10.1.2.80 * /tmp/my.sock Requirement level: Opt-In", "nullable": true, "type": "boolean" }, "network.local.port": { + "default": null, "description": "Local socket port. Useful in case of a multi-port host. Examples: * 65123 Requirement level: Opt-In", "nullable": true, "type": "boolean" }, "network.peer.address": { + "default": null, "description": "Peer address of the network connection - IP address or Unix domain socket name. Examples: * 10.1.2.80 * /tmp/my.sock Requirement level: Recommended", "nullable": true, "type": "boolean" }, "network.peer.port": { + "default": null, "description": "Peer port number of the network connection. Examples: * 65123 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "network.protocol.name": { + "default": null, "description": "OSI application layer or non-OSI equivalent. Examples: * http * spdy Requirement level: Recommended: if not default (http).", "nullable": true, "type": "boolean" }, "network.protocol.version": { + "default": null, "description": "Version of the protocol specified in network.protocol.name. Examples: * 1.0 * 1.1 * 2 * 3 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "network.transport": { + "default": null, "description": "OSI transport layer. Examples: * tcp * udp Requirement level: Conditionally Required", "nullable": true, "type": "boolean" }, "network.type": { + "default": null, "description": "OSI network layer or non-OSI equivalent. Examples: * ipv4 * ipv6 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "server.address": { + "default": null, "description": "Name of the local HTTP server that received the request. Examples: * example.com * 10.1.2.80 * /tmp/my.sock Requirement level: Recommended", "nullable": true, "type": "boolean" }, "server.port": { + "default": null, "description": "Port of the local HTTP server that received the request. Examples: * 80 * 8080 * 443 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "trace_id": { + "default": null, "description": "The OpenTelemetry trace ID. This can be output in logs.", "nullable": true, "type": "boolean" }, "url.path": { + "default": null, "description": "The URI path component Examples: * /search Requirement level: Required", "nullable": true, "type": "boolean" }, "url.query": { + "default": null, "description": "The URI query component Examples: * q=OpenTelemetry Requirement level: Conditionally Required: If and only if one was received/sent.", "nullable": true, "type": "boolean" }, "url.scheme": { + "default": null, "description": "The URI scheme component identifying the used protocol. Examples: * http * https Requirement level: Required", "nullable": true, "type": "boolean" }, "user_agent.original": { + "default": null, "description": "Value of the HTTP User-Agent header sent by the client. Examples: * CERN-LineMode/2.15 * libwww/2.17b3 Requirement level: Recommended", "nullable": true, "type": "boolean" @@ -6563,116 +7248,139 @@ expression: "&schema" "description": "Common attributes for http server and client. See https://opentelemetry.io/docs/specs/semconv/http/http-spans/#common-attributes", "properties": { "baggage": { + "default": null, "description": "All key values from trace baggage.", "nullable": true, "type": "boolean" }, "dd.trace_id": { + "default": null, "description": "The datadog trace ID. This can be output in logs and used to correlate traces in Datadog.", "nullable": true, "type": "boolean" }, "error.type": { + "default": null, "description": "Describes a class of error the operation ended with. Examples: * timeout * name_resolution_error * 500 Requirement level: Conditionally Required: If request has ended with an error.", "nullable": true, "type": "boolean" }, "http.request.body.size": { + "default": null, "description": "The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples: * 3495 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "http.request.method": { + "default": null, "description": "HTTP request method. Examples: * GET * POST * HEAD Requirement level: Required", "nullable": true, "type": "boolean" }, "http.response.body.size": { + "default": null, "description": "The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples: * 3495 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "http.response.status_code": { + "default": null, "description": "HTTP response status code. Examples: * 200 Requirement level: Conditionally Required: If and only if one was received/sent.", "nullable": true, "type": "boolean" }, "http.route": { + "default": null, "description": "The matched route (path template in the format used by the respective server framework). Examples: * /graphql Requirement level: Conditionally Required: If and only if it’s available", "nullable": true, "type": "boolean" }, "network.local.address": { + "default": null, "description": "Local socket address. Useful in case of a multi-IP host. Examples: * 10.1.2.80 * /tmp/my.sock Requirement level: Opt-In", "nullable": true, "type": "boolean" }, "network.local.port": { + "default": null, "description": "Local socket port. Useful in case of a multi-port host. Examples: * 65123 Requirement level: Opt-In", "nullable": true, "type": "boolean" }, "network.peer.address": { + "default": null, "description": "Peer address of the network connection - IP address or Unix domain socket name. Examples: * 10.1.2.80 * /tmp/my.sock Requirement level: Recommended", "nullable": true, "type": "boolean" }, "network.peer.port": { + "default": null, "description": "Peer port number of the network connection. Examples: * 65123 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "network.protocol.name": { + "default": null, "description": "OSI application layer or non-OSI equivalent. Examples: * http * spdy Requirement level: Recommended: if not default (http).", "nullable": true, "type": "boolean" }, "network.protocol.version": { + "default": null, "description": "Version of the protocol specified in network.protocol.name. Examples: * 1.0 * 1.1 * 2 * 3 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "network.transport": { + "default": null, "description": "OSI transport layer. Examples: * tcp * udp Requirement level: Conditionally Required", "nullable": true, "type": "boolean" }, "network.type": { + "default": null, "description": "OSI network layer or non-OSI equivalent. Examples: * ipv4 * ipv6 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "server.address": { + "default": null, "description": "Name of the local HTTP server that received the request. Examples: * example.com * 10.1.2.80 * /tmp/my.sock Requirement level: Recommended", "nullable": true, "type": "boolean" }, "server.port": { + "default": null, "description": "Port of the local HTTP server that received the request. Examples: * 80 * 8080 * 443 Requirement level: Recommended", "nullable": true, "type": "boolean" }, "trace_id": { + "default": null, "description": "The OpenTelemetry trace ID. This can be output in logs.", "nullable": true, "type": "boolean" }, "url.path": { + "default": null, "description": "The URI path component Examples: * /search Requirement level: Required", "nullable": true, "type": "boolean" }, "url.query": { + "default": null, "description": "The URI query component Examples: * q=OpenTelemetry Requirement level: Conditionally Required: If and only if one was received/sent.", "nullable": true, "type": "boolean" }, "url.scheme": { + "default": null, "description": "The URI scheme component identifying the used protocol. Examples: * http * https Requirement level: Required", "nullable": true, "type": "boolean" }, "user_agent.original": { + "default": null, "description": "Value of the HTTP User-Agent header sent by the client. Examples: * CERN-LineMode/2.15 * libwww/2.17b3 Requirement level: Recommended", "nullable": true, "type": "boolean" @@ -6687,21 +7395,25 @@ expression: "&schema" }, "properties": { "subgraph.graphql.document": { + "default": null, "description": "The GraphQL document being executed. Examples: * query findBookById { bookById(id: ?) { name } } Requirement level: Recommended", "nullable": true, "type": "boolean" }, "subgraph.graphql.operation.name": { + "default": null, "description": "The name of the operation being executed. Examples: * findBookById Requirement level: Recommended", "nullable": true, "type": "boolean" }, "subgraph.graphql.operation.type": { + "default": null, "description": "The type of the operation being executed. Examples: * query * subscription * mutation Requirement level: Recommended", "nullable": true, "type": "boolean" }, "subgraph.name": { + "default": null, "description": "The name of the subgraph Examples: * products Requirement level: Required", "nullable": true, "type": "boolean" @@ -6716,21 +7428,25 @@ expression: "&schema" }, "properties": { "subgraph.graphql.document": { + "default": null, "description": "The GraphQL document being executed. Examples: * query findBookById { bookById(id: ?) { name } } Requirement level: Recommended", "nullable": true, "type": "boolean" }, "subgraph.graphql.operation.name": { + "default": null, "description": "The name of the operation being executed. Examples: * findBookById Requirement level: Recommended", "nullable": true, "type": "boolean" }, "subgraph.graphql.operation.type": { + "default": null, "description": "The type of the operation being executed. Examples: * query * subscription * mutation Requirement level: Recommended", "nullable": true, "type": "boolean" }, "subgraph.name": { + "default": null, "description": "The name of the subgraph Examples: * products Requirement level: Required", "nullable": true, "type": "boolean" @@ -6746,36 +7462,43 @@ expression: "&schema" "description": "Attributes for Cost", "properties": { "cost.actual": { + "default": null, "description": "The actual cost of the operation using the currently configured cost model", "nullable": true, "type": "boolean" }, "cost.delta": { + "default": null, "description": "The delta (estimated - actual) cost of the operation using the currently configured cost model", "nullable": true, "type": "boolean" }, "cost.estimated": { + "default": null, "description": "The estimated cost of the operation using the currently configured cost model", "nullable": true, "type": "boolean" }, "cost.result": { + "default": null, "description": "The cost result, this is an error code returned by the cost calculation or COST_OK", "nullable": true, "type": "boolean" }, "graphql.document": { + "default": null, "description": "The GraphQL document being executed. Examples: * query findBookById { bookById(id: ?) { name } } Requirement level: Recommended", "nullable": true, "type": "boolean" }, "graphql.operation.name": { + "default": null, "description": "The name of the operation being executed. Examples: * findBookById Requirement level: Recommended", "nullable": true, "type": "boolean" }, "graphql.operation.type": { + "default": null, "description": "The type of the operation being executed. Examples: * query * subscription * mutation Requirement level: Recommended", "nullable": true, "type": "boolean" @@ -6791,36 +7514,43 @@ expression: "&schema" "description": "Attributes for Cost", "properties": { "cost.actual": { + "default": null, "description": "The actual cost of the operation using the currently configured cost model", "nullable": true, "type": "boolean" }, "cost.delta": { + "default": null, "description": "The delta (estimated - actual) cost of the operation using the currently configured cost model", "nullable": true, "type": "boolean" }, "cost.estimated": { + "default": null, "description": "The estimated cost of the operation using the currently configured cost model", "nullable": true, "type": "boolean" }, "cost.result": { + "default": null, "description": "The cost result, this is an error code returned by the cost calculation or COST_OK", "nullable": true, "type": "boolean" }, "graphql.document": { + "default": null, "description": "The GraphQL document being executed. Examples: * query findBookById { bookById(id: ?) { name } } Requirement level: Recommended", "nullable": true, "type": "boolean" }, "graphql.operation.name": { + "default": null, "description": "The name of the operation being executed. Examples: * findBookById Requirement level: Recommended", "nullable": true, "type": "boolean" }, "graphql.operation.type": { + "default": null, "description": "The type of the operation being executed. Examples: * query * subscription * mutation Requirement level: Recommended", "nullable": true, "type": "boolean" @@ -6891,6 +7621,62 @@ expression: "&schema" }, "type": "object" }, + "extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::GraphQLInstrumentsConfig_apollo_router::plugins::telemetry::config_new::instruments::Instrument": { + "additionalProperties": { + "$ref": "#/definitions/Instrument_for_GraphQLAttributes_and_GraphQLSelector", + "description": "#/definitions/Instrument_for_GraphQLAttributes_and_GraphQLSelector" + }, + "properties": { + "field.execution": { + "$ref": "#/definitions/DefaultedStandardInstrument_for_extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector", + "description": "#/definitions/DefaultedStandardInstrument_for_extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector" + }, + "list.length": { + "$ref": "#/definitions/DefaultedStandardInstrument_for_extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector", + "description": "#/definitions/DefaultedStandardInstrument_for_extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector" + } + }, + "type": "object" + }, + "extendable_attribute_apollo_router::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes_apollo_router::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector": { + "additionalProperties": { + "$ref": "#/definitions/GraphQLSelector", + "description": "#/definitions/GraphQLSelector" + }, + "properties": { + "graphql.field.name": { + "default": null, + "description": "The GraphQL field name", + "nullable": true, + "type": "boolean" + }, + "graphql.field.type": { + "default": null, + "description": "The GraphQL field type", + "nullable": true, + "type": "boolean" + }, + "graphql.list.length": { + "default": null, + "description": "If the field is a list, the length of the list", + "nullable": true, + "type": "boolean" + }, + "graphql.operation.name": { + "default": null, + "description": "The GraphQL operation name", + "nullable": true, + "type": "boolean" + }, + "graphql.type.name": { + "default": null, + "description": "The GraphQL type name", + "nullable": true, + "type": "boolean" + } + }, + "type": "object" + }, "extendable_attribute_apollo_router::plugins::telemetry::config_new::instruments::RouterInstrumentsConfig_apollo_router::plugins::telemetry::config_new::instruments::Instrument": { "additionalProperties": { "$ref": "#/definitions/Instrument_for_RouterAttributes_and_RouterSelector", @@ -7186,10 +7972,6 @@ expression: "&schema" "$ref": "#/definitions/Chaos", "description": "#/definitions/Chaos" }, - "experimental_demand_control": { - "$ref": "#/definitions/DemandControlConfig", - "description": "#/definitions/DemandControlConfig" - }, "experimental_query_planner_mode": { "$ref": "#/definitions/QueryPlannerMode", "description": "#/definitions/QueryPlannerMode" @@ -7235,6 +8017,10 @@ expression: "&schema" "$ref": "#/definitions/Plugins", "description": "#/definitions/Plugins" }, + "preview_demand_control": { + "$ref": "#/definitions/DemandControlConfig", + "description": "#/definitions/DemandControlConfig" + }, "preview_entity_cache": { "$ref": "#/definitions/Config6", "description": "#/definitions/Config6" diff --git a/apollo-router/src/configuration/testdata/metrics/demand_control.router.yaml b/apollo-router/src/configuration/testdata/metrics/demand_control.router.yaml index 2e479c1160..a78a0870ce 100644 --- a/apollo-router/src/configuration/testdata/metrics/demand_control.router.yaml +++ b/apollo-router/src/configuration/testdata/metrics/demand_control.router.yaml @@ -1,4 +1,4 @@ -experimental_demand_control: +preview_demand_control: enabled: true mode: measure strategy: diff --git a/apollo-router/src/configuration/testdata/metrics/telemetry.router.yaml b/apollo-router/src/configuration/testdata/metrics/telemetry.router.yaml index b7942cf3de..b4f9a19dd7 100644 --- a/apollo-router/src/configuration/testdata/metrics/telemetry.router.yaml +++ b/apollo-router/src/configuration/testdata/metrics/telemetry.router.yaml @@ -67,14 +67,14 @@ telemetry: description: "my description" acme.request.size: # The name of your custom instrument/metric value: - request_header: "content-length" + request_header: "content-length" type: counter unit: s - description: "my description" - + description: "my description" + acme.request.length: # The name of your custom instrument/metric value: - request_header: "content-length" + request_header: "content-length" type: histogram unit: s description: "my description" @@ -96,9 +96,12 @@ telemetry: description: "supergraph requests" condition: exists: - subgraph_response_data: "$.products[*].price1" + subgraph_response_data: "$.products[*].price1" attributes: subgraph.name: true + graphql: + list.length: true + field.execution: true events: router: # Standard events diff --git a/apollo-router/src/introspection.rs b/apollo-router/src/introspection.rs index ae575fe9b9..597eb4fa06 100644 --- a/apollo-router/src/introspection.rs +++ b/apollo-router/src/introspection.rs @@ -50,7 +50,7 @@ impl Introspection { /// Execute an introspection and cache the response. pub(crate) async fn execute(&self, query: String) -> Result { - if let Some(response) = self.cache.get(&query).await { + if let Some(response) = self.cache.get(&query, |_| Ok(())).await { return Ok(response); } diff --git a/apollo-router/src/metrics/filter.rs b/apollo-router/src/metrics/filter.rs index 599173b29e..18c19c0746 100644 --- a/apollo-router/src/metrics/filter.rs +++ b/apollo-router/src/metrics/filter.rs @@ -94,7 +94,7 @@ impl FilterMeterProvider { .delegate(delegate) .allow( Regex::new( - r"apollo\.(graphos\.cloud|router\.(operations?|config|schema|query))(\..*|$)", + r"apollo\.(graphos\.cloud|router\.(operations?|lifecycle|config|schema|query|query_planning))(\..*|$)", ) .expect("regex should have been valid"), ) @@ -282,6 +282,14 @@ mod test { .u64_counter("apollo.router.unknown.test") .init() .add(1, &[]); + filtered + .u64_counter("apollo.router.query_planning.test") + .init() + .add(1, &[]); + filtered + .u64_counter("apollo.router.lifecycle.api_schema") + .init() + .add(1, &[]); meter_provider.force_flush(&cx).unwrap(); let metrics: Vec<_> = exporter @@ -304,6 +312,10 @@ mod test { assert!(!metrics .iter() .any(|m| m.name == "apollo.router.unknown.test")); + + assert!(metrics + .iter() + .any(|m| m.name == "apollo.router.lifecycle.api_schema")); } #[tokio::test(flavor = "multi_thread")] diff --git a/apollo-router/src/metrics/mod.rs b/apollo-router/src/metrics/mod.rs index ab270b2f1e..e0deed0c8e 100644 --- a/apollo-router/src/metrics/mod.rs +++ b/apollo-router/src/metrics/mod.rs @@ -349,6 +349,21 @@ pub(crate) mod test_utils { pub(crate) attributes: BTreeMap, } + impl Ord for SerdeMetricDataPoint { + fn cmp(&self, other: &Self) -> Ordering { + //Horribly inefficient, but it's just for testing + let self_string = serde_json::to_string(&self.attributes).expect("serde failed"); + let other_string = serde_json::to_string(&other.attributes).expect("serde failed"); + self_string.cmp(&other_string) + } + } + + impl PartialOrd for SerdeMetricDataPoint { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + impl SerdeMetricData { fn extract_datapoints + Clone + 'static>( metric_data: &mut SerdeMetricData, @@ -374,12 +389,30 @@ pub(crate) mod test_utils { impl From for SerdeMetric { fn from(value: Metric) -> Self { - SerdeMetric { + let mut serde_metric = SerdeMetric { name: value.name.into_owned(), description: value.description.into_owned(), unit: value.unit.as_str().to_string(), data: value.data.into(), + }; + // Sort the datapoints so that we can compare them + serde_metric.data.datapoints.sort(); + + // Redact duration metrics; + if serde_metric.name.ends_with(".duration") { + serde_metric + .data + .datapoints + .iter_mut() + .for_each(|datapoint| { + if let Some(sum) = &datapoint.sum { + if sum.as_f64().unwrap_or_default() > 0.0 { + datapoint.sum = Some(0.1.into()); + } + } + }); } + serde_metric } } diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/mod.rs b/apollo-router/src/plugins/demand_control/cost_calculator/mod.rs index b06323dd86..93313bd9e6 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/mod.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/mod.rs @@ -1,5 +1,5 @@ mod directives; -mod schema_aware_response; +pub(crate) mod schema_aware_response; pub(crate) mod static_cost; use crate::plugins::demand_control::DemandControlError; diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/schema_aware_response.rs b/apollo-router/src/plugins/demand_control/cost_calculator/schema_aware_response.rs index 2ee54a30cd..dca0c46169 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/schema_aware_response.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/schema_aware_response.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use apollo_compiler::ast::NamedType; use apollo_compiler::executable::Field; use apollo_compiler::executable::Selection; use apollo_compiler::executable::SelectionSet; @@ -49,38 +50,83 @@ impl<'a> SchemaAwareResponse<'a> { } } +pub(crate) trait Visitor { + fn visit(&self, value: &TypedValue) { + match value { + TypedValue::Null => self.visit_null(), + TypedValue::Bool(ty, f, b) => self.visit_bool(ty, f, b), + TypedValue::Number(ty, f, n) => self.visit_number(ty, f, n), + TypedValue::String(ty, f, s) => self.visit_string(ty, f, s), + TypedValue::List(ty, f, items) => self.visit_array(ty, f, items), + TypedValue::Object(ty, f, children) => self.visit_object(ty, f, children), + TypedValue::Root(children) => self.visit_root(children), + } + } + fn visit_field(&self, _value: &TypedValue) {} + fn visit_null(&self) {} + fn visit_bool(&self, _ty: &NamedType, _field: &Field, _value: &bool) {} + fn visit_number(&self, _ty: &NamedType, _field: &Field, _value: &serde_json::Number) {} + fn visit_string(&self, _ty: &NamedType, _field: &Field, _value: &str) {} + + fn visit_array(&self, _ty: &NamedType, _field: &Field, items: &[TypedValue]) { + for value in items.iter() { + self.visit_array_element(value); + self.visit(value); + } + } + fn visit_array_element(&self, _value: &TypedValue) {} + fn visit_object( + &self, + _ty: &NamedType, + _field: &Field, + children: &HashMap, + ) { + for value in children.values() { + self.visit_field(value); + self.visit(value); + } + } + fn visit_root(&self, children: &HashMap) { + for value in children.values() { + self.visit_field(value); + self.visit(value); + } + } +} + #[derive(Debug)] pub(crate) enum TypedValue<'a> { Null, - Bool(&'a Field, &'a bool), - Number(&'a Field, &'a serde_json::Number), - String(&'a Field, &'a str), - Array(&'a Field, Vec>), - Object(&'a Field, HashMap>), + Bool(&'a NamedType, &'a Field, &'a bool), + Number(&'a NamedType, &'a Field, &'a serde_json::Number), + String(&'a NamedType, &'a Field, &'a str), + List(&'a NamedType, &'a Field, Vec>), + Object(&'a NamedType, &'a Field, HashMap>), Root(HashMap>), } impl<'a> TypedValue<'a> { fn zip_field( request: &'a ExecutableDocument, + ty: &'a NamedType, field: &'a Field, value: &'a Value, ) -> Result, DemandControlError> { match value { Value::Null => Ok(TypedValue::Null), - Value::Bool(b) => Ok(TypedValue::Bool(field, b)), - Value::Number(n) => Ok(TypedValue::Number(field, n)), - Value::String(s) => Ok(TypedValue::String(field, s.as_str())), + Value::Bool(b) => Ok(TypedValue::Bool(ty, field, b)), + Value::Number(n) => Ok(TypedValue::Number(ty, field, n)), + Value::String(s) => Ok(TypedValue::String(ty, field, s.as_str())), Value::Array(items) => { let mut typed_items = Vec::new(); for item in items { - typed_items.push(TypedValue::zip_field(request, field, item)?); + typed_items.push(TypedValue::zip_field(request, ty, field, item)?); } - Ok(TypedValue::Array(field, typed_items)) + Ok(TypedValue::List(ty, field, typed_items)) } Value::Object(children) => { let typed_children = Self::zip_selections(request, &field.selection_set, children)?; - Ok(TypedValue::Object(field, typed_children)) + Ok(TypedValue::Object(ty, field, typed_children)) } } } @@ -97,7 +143,12 @@ impl<'a> TypedValue<'a> { if let Some(value) = fields.get(inner_field.name.as_str()) { typed_children.insert( inner_field.name.to_string(), - TypedValue::zip_field(request, inner_field.as_ref(), value)?, + TypedValue::zip_field( + request, + &selection_set.ty, + inner_field.as_ref(), + value, + )?, ); } else { tracing::warn!("The response did not include a field corresponding to query field {:?}", inner_field); @@ -135,9 +186,13 @@ mod tests { use apollo_compiler::ExecutableDocument; use apollo_compiler::Schema; use bytes::Bytes; + use insta::assert_yaml_snapshot; + use serde::ser::SerializeMap; + use serde::Serialize; + use serde::Serializer; + use super::*; use crate::graphql::Response; - use crate::plugins::demand_control::cost_calculator::schema_aware_response::SchemaAwareResponse; #[test] fn response_zipper() { @@ -149,8 +204,7 @@ mod tests { let request = ExecutableDocument::parse(&schema, query_str, "").unwrap(); let response = Response::from_bytes("test", Bytes::from_static(response_bytes)).unwrap(); let zipped = SchemaAwareResponse::new(&request, &response); - - assert!(zipped.is_ok()) + insta::with_settings!({sort_maps=>true}, { assert_yaml_snapshot!(zipped.expect("expected zipped response")) }) } #[test] @@ -163,8 +217,7 @@ mod tests { let request = ExecutableDocument::parse(&schema, query_str, "").unwrap(); let response = Response::from_bytes("test", Bytes::from_static(response_bytes)).unwrap(); let zipped = SchemaAwareResponse::new(&request, &response); - - assert!(zipped.is_ok()) + insta::with_settings!({sort_maps=>true}, { assert_yaml_snapshot!(zipped.expect("expected zipped response")) }) } #[test] @@ -177,8 +230,7 @@ mod tests { let request = ExecutableDocument::parse(&schema, query_str, "").unwrap(); let response = Response::from_bytes("test", Bytes::from_static(response_bytes)).unwrap(); let zipped = SchemaAwareResponse::new(&request, &response); - - assert!(zipped.is_ok()) + insta::with_settings!({sort_maps=>true}, { assert_yaml_snapshot!(zipped.expect("expected zipped response")) }) } #[test] @@ -191,7 +243,62 @@ mod tests { let request = ExecutableDocument::parse(&schema, query_str, "").unwrap(); let response = Response::from_bytes("test", Bytes::from_static(response_bytes)).unwrap(); let zipped = SchemaAwareResponse::new(&request, &response); + insta::with_settings!({sort_maps=>true}, { assert_yaml_snapshot!(zipped.expect("expected zipped response")) }) + } - assert!(zipped.is_ok()) + impl Serialize for SchemaAwareResponse<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.value.serialize(serializer) + } + } + + impl Serialize for TypedValue<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + TypedValue::Null => serializer.serialize_none(), + TypedValue::Bool(ty, field, b) => { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("type", ty.as_str())?; + map.serialize_entry("field", &field.name)?; + map.serialize_entry("value", b)?; + map.end() + } + TypedValue::Number(ty, field, n) => { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("type", ty.as_str())?; + map.serialize_entry("field", &field.name)?; + map.serialize_entry("value", n)?; + map.end() + } + TypedValue::String(ty, field, s) => { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("type", ty.as_str())?; + map.serialize_entry("field", &field.name)?; + map.serialize_entry("value", s)?; + map.end() + } + TypedValue::List(ty, field, items) => { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("type", ty.as_str())?; + map.serialize_entry("field", &field.name)?; + map.serialize_entry("items", items)?; + map.end() + } + TypedValue::Object(ty, field, children) => { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("type", ty.as_str())?; + map.serialize_entry("field", &field.name)?; + map.serialize_entry("children", children)?; + map.end() + } + TypedValue::Root(children) => children.serialize(serializer), + } + } } } diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/snapshots/apollo_router__plugins__demand_control__cost_calculator__schema_aware_response__tests__fragment_response_zipper.snap b/apollo-router/src/plugins/demand_control/cost_calculator/snapshots/apollo_router__plugins__demand_control__cost_calculator__schema_aware_response__tests__fragment_response_zipper.snap new file mode 100644 index 0000000000..0866b17ff7 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/snapshots/apollo_router__plugins__demand_control__cost_calculator__schema_aware_response__tests__fragment_response_zipper.snap @@ -0,0 +1,64 @@ +--- +source: apollo-router/src/plugins/demand_control/cost_calculator/schema_aware_response.rs +expression: "zipped.expect(\"expected zipped response\")" +--- +ships: + field: ships + items: + - children: + owner: + children: + licenseNumber: + field: licenseNumber + type: User + value: 1 + name: + field: name + type: User + value: Kate Chopin + field: owner + type: Ship + field: ships + type: Query + - children: + owner: + children: + licenseNumber: + field: licenseNumber + type: User + value: 2 + name: + field: name + type: User + value: Paul Auster + field: owner + type: Ship + field: ships + type: Query + type: Query +users: + field: users + items: + - children: + licenseNumber: + field: licenseNumber + type: User + value: 1 + name: + field: name + type: User + value: Kate Chopin + field: users + type: Query + - children: + licenseNumber: + field: licenseNumber + type: User + value: 2 + name: + field: name + type: User + value: Paul Auster + field: users + type: Query + type: Query diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/snapshots/apollo_router__plugins__demand_control__cost_calculator__schema_aware_response__tests__inline_fragment_zipper.snap b/apollo-router/src/plugins/demand_control/cost_calculator/snapshots/apollo_router__plugins__demand_control__cost_calculator__schema_aware_response__tests__inline_fragment_zipper.snap new file mode 100644 index 0000000000..0866b17ff7 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/snapshots/apollo_router__plugins__demand_control__cost_calculator__schema_aware_response__tests__inline_fragment_zipper.snap @@ -0,0 +1,64 @@ +--- +source: apollo-router/src/plugins/demand_control/cost_calculator/schema_aware_response.rs +expression: "zipped.expect(\"expected zipped response\")" +--- +ships: + field: ships + items: + - children: + owner: + children: + licenseNumber: + field: licenseNumber + type: User + value: 1 + name: + field: name + type: User + value: Kate Chopin + field: owner + type: Ship + field: ships + type: Query + - children: + owner: + children: + licenseNumber: + field: licenseNumber + type: User + value: 2 + name: + field: name + type: User + value: Paul Auster + field: owner + type: Ship + field: ships + type: Query + type: Query +users: + field: users + items: + - children: + licenseNumber: + field: licenseNumber + type: User + value: 1 + name: + field: name + type: User + value: Kate Chopin + field: users + type: Query + - children: + licenseNumber: + field: licenseNumber + type: User + value: 2 + name: + field: name + type: User + value: Paul Auster + field: users + type: Query + type: Query diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/snapshots/apollo_router__plugins__demand_control__cost_calculator__schema_aware_response__tests__named_operation_zipper.snap b/apollo-router/src/plugins/demand_control/cost_calculator/snapshots/apollo_router__plugins__demand_control__cost_calculator__schema_aware_response__tests__named_operation_zipper.snap new file mode 100644 index 0000000000..f2f58ee726 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/snapshots/apollo_router__plugins__demand_control__cost_calculator__schema_aware_response__tests__named_operation_zipper.snap @@ -0,0 +1,30 @@ +--- +source: apollo-router/src/plugins/demand_control/cost_calculator/schema_aware_response.rs +expression: "zipped.expect(\"expected zipped response\")" +--- +users: + field: users + items: + - children: + licenseNumber: + field: licenseNumber + type: User + value: 1 + name: + field: name + type: User + value: Kate Chopin + field: users + type: Query + - children: + licenseNumber: + field: licenseNumber + type: User + value: 2 + name: + field: name + type: User + value: Paul Auster + field: users + type: Query + type: Query diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/snapshots/apollo_router__plugins__demand_control__cost_calculator__schema_aware_response__tests__response_zipper.snap b/apollo-router/src/plugins/demand_control/cost_calculator/snapshots/apollo_router__plugins__demand_control__cost_calculator__schema_aware_response__tests__response_zipper.snap new file mode 100644 index 0000000000..ee8efc2cf5 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/snapshots/apollo_router__plugins__demand_control__cost_calculator__schema_aware_response__tests__response_zipper.snap @@ -0,0 +1,30 @@ +--- +source: apollo-router/src/plugins/demand_control/cost_calculator/schema_aware_response.rs +expression: "zipped.expect(\"expected zipped response\")" +--- +ships: + field: ships + items: + - children: + name: + field: name + type: Ship + value: Boaty McBoatface + registrationFee: + field: registrationFee + type: Ship + value: 200 + field: ships + type: Query + - children: + name: + field: name + type: Ship + value: HMS Grapherson + registrationFee: + field: registrationFee + type: Ship + value: 150 + field: ships + type: Query + type: Query diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs index 6b713d19bf..f30dfd21da 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs @@ -259,8 +259,8 @@ impl StaticCostCalculator { })?; let operation = operation - .as_parsed(schema) - .map_err(DemandControlError::InvalidSubgraphQuery)?; + .as_parsed() + .map_err(DemandControlError::SubgraphOperationNotInitialized)?; self.estimated(operation, schema) } @@ -309,11 +309,11 @@ impl StaticCostCalculator { fn score_json(value: &TypedValue) -> Result { match value { TypedValue::Null => Ok(0.0), - TypedValue::Bool(_, _) => Ok(0.0), - TypedValue::Number(_, _) => Ok(0.0), - TypedValue::String(_, _) => Ok(0.0), - TypedValue::Array(_, items) => Self::summed_score_of_values(items), - TypedValue::Object(_, children) => { + TypedValue::Bool(_, _, _) => Ok(0.0), + TypedValue::Number(_, _, _) => Ok(0.0), + TypedValue::String(_, _, _) => Ok(0.0), + TypedValue::List(_, _, items) => Self::summed_score_of_values(items), + TypedValue::Object(_, _, children) => { let cost_of_children = Self::summed_score_of_values(children.values())?; Ok(1.0 + cost_of_children) } diff --git a/apollo-router/src/plugins/demand_control/fixtures/enforce_on_execution_request.router.yaml b/apollo-router/src/plugins/demand_control/fixtures/enforce_on_execution_request.router.yaml index dffa5e6951..43b492c8ad 100644 --- a/apollo-router/src/plugins/demand_control/fixtures/enforce_on_execution_request.router.yaml +++ b/apollo-router/src/plugins/demand_control/fixtures/enforce_on_execution_request.router.yaml @@ -1,4 +1,4 @@ -experimental_demand_control: +preview_demand_control: enabled: true mode: enforce strategy: diff --git a/apollo-router/src/plugins/demand_control/fixtures/enforce_on_execution_response.router.yaml b/apollo-router/src/plugins/demand_control/fixtures/enforce_on_execution_response.router.yaml index e4a954cbcd..deb3908da5 100644 --- a/apollo-router/src/plugins/demand_control/fixtures/enforce_on_execution_response.router.yaml +++ b/apollo-router/src/plugins/demand_control/fixtures/enforce_on_execution_response.router.yaml @@ -1,4 +1,4 @@ -experimental_demand_control: +preview_demand_control: enabled: true mode: enforce strategy: diff --git a/apollo-router/src/plugins/demand_control/fixtures/enforce_on_subgraph_request.router.yaml b/apollo-router/src/plugins/demand_control/fixtures/enforce_on_subgraph_request.router.yaml index 731fffb8f9..dc83e08c34 100644 --- a/apollo-router/src/plugins/demand_control/fixtures/enforce_on_subgraph_request.router.yaml +++ b/apollo-router/src/plugins/demand_control/fixtures/enforce_on_subgraph_request.router.yaml @@ -1,4 +1,4 @@ -experimental_demand_control: +preview_demand_control: enabled: true mode: enforce strategy: diff --git a/apollo-router/src/plugins/demand_control/fixtures/enforce_on_subgraph_response.router.yaml b/apollo-router/src/plugins/demand_control/fixtures/enforce_on_subgraph_response.router.yaml index ff97fd8c25..56fd39e585 100644 --- a/apollo-router/src/plugins/demand_control/fixtures/enforce_on_subgraph_response.router.yaml +++ b/apollo-router/src/plugins/demand_control/fixtures/enforce_on_subgraph_response.router.yaml @@ -1,4 +1,4 @@ -experimental_demand_control: +preview_demand_control: enabled: true mode: enforce strategy: diff --git a/apollo-router/src/plugins/demand_control/fixtures/measure_on_execution_request.router.yaml b/apollo-router/src/plugins/demand_control/fixtures/measure_on_execution_request.router.yaml index 18ddc41e57..c96a6908bc 100644 --- a/apollo-router/src/plugins/demand_control/fixtures/measure_on_execution_request.router.yaml +++ b/apollo-router/src/plugins/demand_control/fixtures/measure_on_execution_request.router.yaml @@ -1,4 +1,4 @@ -experimental_demand_control: +preview_demand_control: enabled: true mode: measure strategy: diff --git a/apollo-router/src/plugins/demand_control/fixtures/measure_on_execution_response.router.yaml b/apollo-router/src/plugins/demand_control/fixtures/measure_on_execution_response.router.yaml index efcc67c8df..a7422da35b 100644 --- a/apollo-router/src/plugins/demand_control/fixtures/measure_on_execution_response.router.yaml +++ b/apollo-router/src/plugins/demand_control/fixtures/measure_on_execution_response.router.yaml @@ -1,4 +1,4 @@ -experimental_demand_control: +preview_demand_control: enabled: true mode: measure strategy: diff --git a/apollo-router/src/plugins/demand_control/fixtures/measure_on_subgraph_request.router.yaml b/apollo-router/src/plugins/demand_control/fixtures/measure_on_subgraph_request.router.yaml index 18ddc41e57..c96a6908bc 100644 --- a/apollo-router/src/plugins/demand_control/fixtures/measure_on_subgraph_request.router.yaml +++ b/apollo-router/src/plugins/demand_control/fixtures/measure_on_subgraph_request.router.yaml @@ -1,4 +1,4 @@ -experimental_demand_control: +preview_demand_control: enabled: true mode: measure strategy: diff --git a/apollo-router/src/plugins/demand_control/fixtures/measure_on_subgraph_response.router.yaml b/apollo-router/src/plugins/demand_control/fixtures/measure_on_subgraph_response.router.yaml index efcc67c8df..a7422da35b 100644 --- a/apollo-router/src/plugins/demand_control/fixtures/measure_on_subgraph_response.router.yaml +++ b/apollo-router/src/plugins/demand_control/fixtures/measure_on_subgraph_response.router.yaml @@ -1,4 +1,4 @@ -experimental_demand_control: +preview_demand_control: enabled: true mode: measure strategy: diff --git a/apollo-router/src/plugins/demand_control/mod.rs b/apollo-router/src/plugins/demand_control/mod.rs index 0087e193cc..ea698b1f9b 100644 --- a/apollo-router/src/plugins/demand_control/mod.rs +++ b/apollo-router/src/plugins/demand_control/mod.rs @@ -20,7 +20,6 @@ use tower::ServiceBuilder; use tower::ServiceExt; use crate::error::Error; -use crate::error::ValidationErrors; use crate::graphql; use crate::graphql::IntoGraphQLErrors; use crate::json_ext::Object; @@ -138,10 +137,10 @@ pub(crate) enum DemandControlError { }, /// Query could not be parsed: {0} QueryParseFailure(String), - /// Invalid subgraph query: {0} - InvalidSubgraphQuery(ValidationErrors), /// The response body could not be properly matched with its query's structure: {0} ResponseTypingFailure(String), + /// {0} + SubgraphOperationNotInitialized(crate::query_planner::fetch::SubgraphOperationNotInitialized), } impl IntoGraphQLErrors for DemandControlError { @@ -181,9 +180,7 @@ impl IntoGraphQLErrors for DemandControlError { .extension_code(self.code()) .message(self.to_string()) .build()]), - DemandControlError::InvalidSubgraphQuery(errors) => { - Ok(errors.into_graphql_errors_infallible()) - } + DemandControlError::SubgraphOperationNotInitialized(e) => Ok(e.into_graphql_errors()), } } } @@ -195,7 +192,7 @@ impl DemandControlError { DemandControlError::ActualCostTooExpensive { .. } => "COST_ACTUAL_TOO_EXPENSIVE", DemandControlError::QueryParseFailure(_) => "COST_QUERY_PARSE_FAILURE", DemandControlError::ResponseTypingFailure(_) => "COST_RESPONSE_TYPING_FAILURE", - DemandControlError::InvalidSubgraphQuery(_) => "GRAPHQL_VALIDATION_FAILED", + DemandControlError::SubgraphOperationNotInitialized(e) => e.code(), } } } @@ -391,7 +388,7 @@ impl Plugin for DemandControl { } } -register_plugin!("apollo", "experimental_demand_control", DemandControl); +register_plugin!("apollo", "preview_demand_control", DemandControl); #[cfg(test)] mod test { diff --git a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap index d680c0b53f..cb657dcdce 100644 --- a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap +++ b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap @@ -18,6 +18,7 @@ expression: query_plan "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "12dda6193654ae4fe6e38bc09d4f81cc73d0c9e098692096f72d2158eef4776f", "authorization": { "is_authenticated": false, diff --git a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap index 612b147fc2..d18a3e2b11 100644 --- a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap +++ b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap @@ -23,6 +23,7 @@ expression: query_plan "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "00ad582ea45fc1bce436b36b21512f3d2c47b74fdbdc61e4b349289722c9ecf2", "authorization": { "is_authenticated": false, @@ -61,6 +62,7 @@ expression: query_plan "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "a8ebdc2151a2e5207882e43c6906c0c64167fd9a8e0c7c4becc47736a5105096", "authorization": { "is_authenticated": false, diff --git a/apollo-router/src/plugins/rhai/engine.rs b/apollo-router/src/plugins/rhai/engine.rs index 7d777a343c..4b576124a0 100644 --- a/apollo-router/src/plugins/rhai/engine.rs +++ b/apollo-router/src/plugins/rhai/engine.rs @@ -47,6 +47,7 @@ use crate::http_ext; use crate::plugins::authentication::APOLLO_AUTHENTICATION_JWT_CLAIMS; use crate::plugins::cache::entity::CONTEXT_CACHE_KEY; use crate::plugins::subscription::SUBSCRIPTION_WS_CUSTOM_CONNECTION_PARAMS; +use crate::query_planner::APOLLO_OPERATION_ID; use crate::Context; const CANNOT_ACCESS_HEADERS_ON_A_DEFERRED_RESPONSE: &str = @@ -1756,6 +1757,7 @@ impl Rhai { SUBSCRIPTION_WS_CUSTOM_CONNECTION_PARAMS.to_string().into(), ); global_variables.insert("APOLLO_ENTITY_CACHE_KEY".into(), CONTEXT_CACHE_KEY.into()); + global_variables.insert("APOLLO_OPERATION_ID".into(), APOLLO_OPERATION_ID.into()); let shared_globals = Arc::new(global_variables); diff --git a/apollo-router/src/plugins/rhai/tests.rs b/apollo-router/src/plugins/rhai/tests.rs index 5b1b5fd771..f189aab02d 100644 --- a/apollo-router/src/plugins/rhai/tests.rs +++ b/apollo-router/src/plugins/rhai/tests.rs @@ -169,7 +169,7 @@ fn new_rhai_test_engine() -> Engine { #[test] fn it_logs_messages() { let env_filter = "apollo_router=trace"; - let mock_writer = tracing_test::internal::MockWriter::new(&tracing_test::internal::GLOBAL_BUF); + let mock_writer = tracing_test::internal::MockWriter::new(tracing_test::internal::global_buf()); let subscriber = tracing_test::internal::get_subscriber(mock_writer, env_filter); let _guard = tracing::dispatcher::set_default(&subscriber); @@ -209,7 +209,7 @@ fn it_logs_messages() { #[test] fn it_prints_messages_to_log() { let env_filter = "apollo_router=trace"; - let mock_writer = tracing_test::internal::MockWriter::new(&tracing_test::internal::GLOBAL_BUF); + let mock_writer = tracing_test::internal::MockWriter::new(tracing_test::internal::global_buf()); let subscriber = tracing_test::internal::get_subscriber(mock_writer, env_filter); let _guard = tracing::dispatcher::set_default(&subscriber); diff --git a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap index b32a905c0e..0d6ab611f6 100644 --- a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap +++ b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap @@ -68,6 +68,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "7245d488e97c3b2ac9f5fa4dd4660940b94ad81af070013305b2c0f76337b2f9", "authorization": { "is_authenticated": false, @@ -107,6 +108,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "6e0b4156706ea0cf924500cfdc99dd44b9f0ed07e2d3f888d4aff156e6a33238", "authorization": { "is_authenticated": false, @@ -153,6 +155,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "ff649f3d70241d5a8cd5f5d03ff4c41ecff72b0e4129a480207b05ac92318042", "authorization": { "is_authenticated": false, @@ -196,6 +199,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "bf9f3beda78a7a565e47c862157bad4ec871d724d752218da1168455dddca074", "authorization": { "is_authenticated": false, diff --git a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap index b32a905c0e..0d6ab611f6 100644 --- a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap +++ b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap @@ -68,6 +68,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "7245d488e97c3b2ac9f5fa4dd4660940b94ad81af070013305b2c0f76337b2f9", "authorization": { "is_authenticated": false, @@ -107,6 +108,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "6e0b4156706ea0cf924500cfdc99dd44b9f0ed07e2d3f888d4aff156e6a33238", "authorization": { "is_authenticated": false, @@ -153,6 +155,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "ff649f3d70241d5a8cd5f5d03ff4c41ecff72b0e4129a480207b05ac92318042", "authorization": { "is_authenticated": false, @@ -196,6 +199,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "bf9f3beda78a7a565e47c862157bad4ec871d724d752218da1168455dddca074", "authorization": { "is_authenticated": false, diff --git a/apollo-router/src/plugins/telemetry/config_new/conditional.rs b/apollo-router/src/plugins/telemetry/config_new/conditional.rs index 124e41b853..46c1e42d01 100644 --- a/apollo-router/src/plugins/telemetry/config_new/conditional.rs +++ b/apollo-router/src/plugins/telemetry/config_new/conditional.rs @@ -18,6 +18,7 @@ use serde::Deserializer; use serde_json::Map; use serde_json::Value; +use crate::plugins::demand_control::cost_calculator::schema_aware_response::TypedValue; use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel; use crate::plugins::telemetry::config_new::conditions::Condition; use crate::plugins::telemetry::config_new::DefaultForLevel; @@ -205,7 +206,6 @@ where ) -> Option { // We may have got the value from the request. let value = mem::take(&mut *self.value.lock()); - match (value, &self.condition) { (State::Value(value), Some(condition)) => { // We have a value already, let's see if the condition was evaluated to true. @@ -291,6 +291,40 @@ where _ => None, } } + + fn on_response_field( + &self, + typed_value: &TypedValue, + ctx: &Context, + ) -> Option { + // We may have got the value from the request. + let value = mem::take(&mut *self.value.lock()); + + match (value, &self.condition) { + (State::Value(value), Some(condition)) => { + // We have a value already, let's see if the condition was evaluated to true. + if condition.lock().evaluate_response_field(typed_value, ctx) { + *self.value.lock() = State::Returned; + Some(value) + } else { + None + } + } + (State::Pending, Some(condition)) => { + // We don't have a value already, let's try to get it from the error if the condition was evaluated to true. + if condition.lock().evaluate_response_field(typed_value, ctx) { + self.selector.on_response_field(typed_value, ctx) + } else { + None + } + } + (State::Pending, None) => { + // We don't have a value already, and there is no condition. + self.selector.on_response_field(typed_value, ctx) + } + _ => None, + } + } } /// Custom Deserializer for attributes that will deserialize into a custom field if possible, but otherwise into one of the pre-defined attributes. diff --git a/apollo-router/src/plugins/telemetry/config_new/conditions.rs b/apollo-router/src/plugins/telemetry/config_new/conditions.rs index 10903b9bad..8bb41eaf30 100644 --- a/apollo-router/src/plugins/telemetry/config_new/conditions.rs +++ b/apollo-router/src/plugins/telemetry/config_new/conditions.rs @@ -3,6 +3,7 @@ use schemars::JsonSchema; use serde::Deserialize; use tower::BoxError; +use crate::plugins::demand_control::cost_calculator::schema_aware_response::TypedValue; use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config_new::Selector; use crate::Context; @@ -267,6 +268,132 @@ where Condition::False => false, } } + + pub(crate) fn evaluate_response_field(&self, typed_value: &TypedValue, ctx: &Context) -> bool { + match self { + Condition::Eq(eq) => { + let left = eq[0].on_response_field(typed_value, ctx); + let right = eq[1].on_response_field(typed_value, ctx); + left == right + } + Condition::Gt(gt) => { + let left_att = gt[0] + .on_response_field(typed_value, ctx) + .map(AttributeValue::from); + let right_att = gt[1] + .on_response_field(typed_value, ctx) + .map(AttributeValue::from); + left_att.zip(right_att).map_or(false, |(l, r)| l > r) + } + Condition::Lt(gt) => { + let left_att = gt[0] + .on_response_field(typed_value, ctx) + .map(AttributeValue::from); + let right_att = gt[1] + .on_response_field(typed_value, ctx) + .map(AttributeValue::from); + left_att.zip(right_att).map_or(false, |(l, r)| l < r) + } + Condition::Exists(exist) => exist.on_response_field(typed_value, ctx).is_some(), + Condition::All(all) => all + .iter() + .all(|c| c.evaluate_response_field(typed_value, ctx)), + Condition::Any(any) => any + .iter() + .any(|c| c.evaluate_response_field(typed_value, ctx)), + Condition::Not(not) => !not.evaluate_response_field(typed_value, ctx), + Condition::True => true, + Condition::False => false, + } + } + pub(crate) fn evaluate_drop(&self) -> Option { + match self { + Condition::Eq(eq) => match (eq[0].on_drop(), eq[1].on_drop()) { + (Some(left), Some(right)) => { + if left == right { + Some(true) + } else { + Some(false) + } + } + _ => None, + }, + Condition::Gt(gt) => { + let left_att = gt[0].on_drop().map(AttributeValue::from); + let right_att = gt[1].on_drop().map(AttributeValue::from); + match (left_att, right_att) { + (Some(l), Some(r)) => { + if l > r { + Some(true) + } else { + Some(false) + } + } + _ => None, + } + } + Condition::Lt(lt) => { + let left_att = lt[0].on_drop().map(AttributeValue::from); + let right_att = lt[1].on_drop().map(AttributeValue::from); + match (left_att, right_att) { + (Some(l), Some(r)) => { + if l < r { + Some(true) + } else { + Some(false) + } + } + _ => None, + } + } + Condition::Exists(exist) => { + if exist.on_drop().is_some() { + Some(true) + } else { + None + } + } + Condition::All(all) => { + if all.is_empty() { + return Some(true); + } + let mut response = Some(true); + for cond in all { + match cond.evaluate_drop() { + Some(resp) => { + response = response.map(|r| resp && r); + } + None => { + response = None; + } + } + } + + response + } + Condition::Any(any) => { + if any.is_empty() { + return Some(true); + } + let mut response: Option = Some(false); + for cond in any { + match cond.evaluate_drop() { + Some(resp) => { + response = response.map(|r| resp || r); + } + None => { + response = None; + } + } + } + + response + } + Condition::Not(not) => not.evaluate_drop().map(|r| !r), + Condition::True => Some(true), + Condition::False => Some(false), + } + } } impl Selector for SelectorOrValue @@ -304,59 +431,54 @@ where SelectorOrValue::Selector(selector) => selector.on_error(error), } } + + fn on_response_field(&self, typed_value: &TypedValue, ctx: &Context) -> Option { + match self { + SelectorOrValue::Value(value) => Some(value.clone().into()), + SelectorOrValue::Selector(selector) => selector.on_response_field(typed_value, ctx), + } + } + + fn on_drop(&self) -> Option { + match self { + SelectorOrValue::Value(value) => Some(value.clone().into()), + SelectorOrValue::Selector(selector) => selector.on_drop(), + } + } } #[cfg(test)] mod test { use opentelemetry::Value; use tower::BoxError; + use TestSelector::Req; + use TestSelector::Resp; + use TestSelector::Static; + use crate::plugins::demand_control::cost_calculator::schema_aware_response::TypedValue; use crate::plugins::telemetry::config_new::conditions::Condition; use crate::plugins::telemetry::config_new::conditions::SelectorOrValue; + use crate::plugins::telemetry::config_new::test::field; + use crate::plugins::telemetry::config_new::test::ty; use crate::plugins::telemetry::config_new::Selector; use crate::Context; - struct TestSelector; - impl Selector for TestSelector { - type Request = Option; - type Response = Option; - type EventResponse = Option; - - fn on_request(&self, request: &Self::Request) -> Option { - request.map(Value::I64) - } - - fn on_response(&self, response: &Self::Response) -> Option { - response.map(Value::I64) - } - - fn on_error(&self, error: &tower::BoxError) -> Option { - Some(error.to_string().into()) - } - - fn on_response_event( - &self, - response: &Self::EventResponse, - _ctx: &crate::Context, - ) -> Option { - response.map(Value::I64) - } - } - - enum TestSelectorReqRes { + enum TestSelector { Req, Resp, + Static(i64), } - impl Selector for TestSelectorReqRes { + impl Selector for TestSelector { type Request = Option; type Response = Option; type EventResponse = Option; fn on_request(&self, request: &Self::Request) -> Option { match self { - TestSelectorReqRes::Req => request.map(Value::I64), - TestSelectorReqRes::Resp => None, + Req => request.map(Value::I64), + Resp => None, + Static(v) => Some((*v).into()), } } @@ -366,444 +488,349 @@ mod test { _ctx: &crate::Context, ) -> Option { match self { - TestSelectorReqRes::Req => None, - TestSelectorReqRes::Resp => response.map(Value::I64), + Req => None, + Resp => response.map(Value::I64), + Static(v) => Some((*v).into()), } } fn on_response(&self, response: &Self::Response) -> Option { match self { - TestSelectorReqRes::Req => None, - TestSelectorReqRes::Resp => response.map(Value::I64), + Req => None, + Resp => response.map(Value::I64), + Static(v) => Some((*v).into()), } } fn on_error(&self, error: &tower::BoxError) -> Option { - Some(error.to_string().into()) + if error.to_string() != "" { + Some("error".into()) + } else { + None + } + } + + fn on_response_field(&self, typed_value: &TypedValue, _ctx: &Context) -> Option { + if let TypedValue::Number(_name, _ty, val) = typed_value { + Some(Value::I64(val.as_i64().expect("mut be i64"))) + } else { + None + } + } + + fn on_drop(&self) -> Option { + match self { + Static(v) => Some((*v).into()), + _ => None, + } } } #[test] fn test_condition_exist() { - assert_eq!( - None, - Condition::::Exists(TestSelectorReqRes::Req) - .evaluate_request(&None) - ); - assert!( - !Condition::::Exists(TestSelectorReqRes::Req) - .evaluate_response(&Some(3i64)) - ); - assert_eq!( - Some(true), - Condition::::Exists(TestSelectorReqRes::Req) - .evaluate_request(&Some(2i64)) - ); - assert!( - Condition::::Exists(TestSelectorReqRes::Resp) - .evaluate_response(&Some(3i64)) - ); - assert!( - Condition::::Exists(TestSelectorReqRes::Resp) - .evaluate_event_response(&Some(3i64), &Context::new()) - ); + assert_eq!(exists(Req).req(None), None); + assert_eq!(exists(Req).req(Some(1i64)), Some(true)); + assert!(!exists(Resp).resp(None)); + assert!(exists(Resp).resp(Some(1i64))); + assert!(!exists(Resp).resp_event(None)); + assert!(exists(Resp).resp_event(Some(1i64))); + assert!(!exists(Resp).error(None)); + assert!(exists(Resp).error(Some("error"))); + assert!(!exists(Resp).field(None)); + assert!(exists(Resp).field(Some(1i64))); } #[test] fn test_condition_eq() { - assert_eq!( - Some(true), - Condition::::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(1i64.into()), - ]) - .evaluate_request(&None) - ); - assert!(Condition::::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(1i64.into()), - ]) - .evaluate_response(&None)); - assert!(!Condition::::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(2i64.into()), - ]) - .evaluate_response(&None)); - assert!(Condition::::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(1i64.into()), - ]) - .evaluate_event_response(&None, &Context::new())); - assert!(!Condition::::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(2i64.into()), - ]) - .evaluate_event_response(&None, &Context::new())); + assert_eq!(eq(1, 2).req(None), Some(false)); + assert_eq!(eq(1, 1).req(None), Some(true)); + assert!(!eq(1, 2).resp(None)); + assert!(eq(1, 1).resp(None)); + assert!(!eq(1, 2).resp_event(None)); + assert!(eq(1, 1).resp_event(None)); + assert!(!eq(1, 2).error(None)); + assert!(eq(1, 1).error(None)); + assert!(!eq(1, 2).field(None)); + assert!(eq(1, 1).field(None)); } #[test] - fn test_condition_eq_selector() { - assert_eq!( - Some(true), - Condition::::Eq([ - SelectorOrValue::Selector(TestSelector), - SelectorOrValue::Value(1i64.into()), - ]) - .evaluate_request(&Some(1i64)) - ); - assert_eq!( - Some(true), - Condition::::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Selector(TestSelector), - ]) - .evaluate_request(&Some(1i64)) - ); - - assert!(Condition::::Eq([ - SelectorOrValue::Selector(TestSelector), - SelectorOrValue::Value(2i64.into()), - ]) - .evaluate_request(&None) - .is_none()); - - assert!(Condition::::Eq([ - SelectorOrValue::Selector(TestSelector), - SelectorOrValue::Value(2i64.into()), - ]) - .evaluate_response(&Some(2i64))); - assert!(Condition::::Eq([ - SelectorOrValue::Value(2i64.into()), - SelectorOrValue::Selector(TestSelector), - ]) - .evaluate_response(&Some(2i64))); - - assert!(!Condition::::Eq([ - SelectorOrValue::Selector(TestSelector), - SelectorOrValue::Value(3i64.into()), - ]) - .evaluate_response(&None)); - assert!(!Condition::::Eq([ - SelectorOrValue::Value(3i64.into()), - SelectorOrValue::Selector(TestSelector), - ]) - .evaluate_response(&None)); - - assert!(Condition::::Eq([ - SelectorOrValue::Selector(TestSelector), - SelectorOrValue::Value(2i64.into()), - ]) - .evaluate_event_response(&Some(2i64), &Context::new())); - assert!(Condition::::Eq([ - SelectorOrValue::Value(2i64.into()), - SelectorOrValue::Selector(TestSelector), - ]) - .evaluate_event_response(&Some(2i64), &Context::new())); - - assert!(!Condition::::Eq([ - SelectorOrValue::Selector(TestSelector), - SelectorOrValue::Value(3i64.into()), - ]) - .evaluate_event_response(&None, &Context::new())); - assert!(!Condition::::Eq([ - SelectorOrValue::Value(3i64.into()), - SelectorOrValue::Selector(TestSelector), - ]) - .evaluate_event_response(&None, &Context::new())); - assert!(Condition::::Eq([ - SelectorOrValue::Value("my custom error".to_string().into()), - SelectorOrValue::Selector(TestSelector), - ]) - .evaluate_error(&BoxError::from("my custom error"))); - assert!(!Condition::::Eq([ - SelectorOrValue::Value("my custom error".to_string().into()), - SelectorOrValue::Selector(TestSelector), - ]) - .evaluate_error(&BoxError::from("my different custom error"))); - - let mut condition = Condition::::Eq([ - SelectorOrValue::Value(3i64.into()), - SelectorOrValue::Selector(TestSelectorReqRes::Req), - ]); - assert_eq!(Some(false), condition.evaluate_request(&Some(2i64))); - - assert_eq!( - Some(true), - Condition::::Eq([ - SelectorOrValue::Value("a".into()), - SelectorOrValue::Value("a".into()) - ]) - .evaluate_request(&None) - ); + fn test_condition_gt() { + test_gt(2, 1, 1); + test_gt(2.0, 1.0, 1.0); + test_gt("b", "a", "a"); + assert_eq!(gt(true, false).req(None), Some(true)); + assert_eq!(gt(false, true).req(None), Some(false)); + assert_eq!(gt(true, true).req(None), Some(false)); + assert_eq!(gt(false, false).req(None), Some(false)); + } + + fn test_gt(l1: T, l2: T, r: T) + where + T: Into> + Clone, + { + assert_eq!(gt(l1.clone(), r.clone()).req(None), Some(true)); + assert_eq!(gt(l2.clone(), r.clone()).req(None), Some(false)); + assert!(gt(l1.clone(), r.clone()).resp(None)); + assert!(!gt(l2.clone(), r.clone()).resp(None)); + assert!(gt(l1.clone(), r.clone()).resp_event(None)); + assert!(!gt(l2.clone(), r.clone()).resp_event(None)); + assert!(gt(l1.clone(), r.clone()).error(None)); + assert!(!gt(l2.clone(), r.clone()).error(None)); + assert!(gt(l1.clone(), r.clone()).field(None)); + assert!(!gt(l2.clone(), r.clone()).field(None)); } #[test] - fn test_condition_gt() { - assert_eq!( - Some(true), - Condition::::Gt([ - SelectorOrValue::Value(true.into()), - SelectorOrValue::Value(false.into()) - ]) - .evaluate_request(&None) - ); - - assert_eq!( - Some(false), - Condition::::Gt([ - SelectorOrValue::Value(true.into()), - SelectorOrValue::Value(true.into()) - ]) - .evaluate_request(&None) - ); - - assert_eq!( - Some(true), - Condition::::Gt([ - SelectorOrValue::Value(2f64.into()), - SelectorOrValue::Value(1f64.into()) - ]) - .evaluate_request(&None) - ); - - assert_eq!( - Some(false), - Condition::::Gt([ - SelectorOrValue::Value(1f64.into()), - SelectorOrValue::Value(1f64.into()) - ]) - .evaluate_request(&None) - ); - - assert_eq!( - Some(true), - Condition::::Gt([ - SelectorOrValue::Value(2i64.into()), - SelectorOrValue::Value(1i64.into()) - ]) - .evaluate_request(&None) - ); - - assert_eq!( - Some(false), - Condition::::Gt([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(1i64.into()) - ]) - .evaluate_request(&None) - ); - - assert_eq!( - Some(true), - Condition::::Gt([ - SelectorOrValue::Value("b".into()), - SelectorOrValue::Value("a".into()) - ]) - .evaluate_request(&None) - ); - - assert_eq!( - Some(false), - Condition::::Gt([ - SelectorOrValue::Value("a".into()), - SelectorOrValue::Value("a".into()) - ]) - .evaluate_request(&None) - ); - - assert_eq!( - Some(true), - Condition::::Gt([ - SelectorOrValue::Value(2i64.into()), - SelectorOrValue::Selector(TestSelector) - ]) - .evaluate_request(&Some(1i64)) - ); - - assert_eq!( - Some(false), - Condition::::Gt([ - SelectorOrValue::Selector(TestSelector), - SelectorOrValue::Value(1i64.into()) - ]) - .evaluate_request(&Some(1i64)) - ); - - assert!(Condition::::Gt([ - SelectorOrValue::Value(2i64.into()), - SelectorOrValue::Value(1i64.into()) - ]) - .evaluate_response(&None)); + fn test_condition_lt() { + test_lt(1, 2, 2); + test_lt(1.0, 2.0, 2.0); + test_lt("a", "b", "b"); + assert_eq!(lt(true, false).req(None), Some(false)); + assert_eq!(lt(false, true).req(None), Some(true)); + assert_eq!(lt(true, true).req(None), Some(false)); + assert_eq!(lt(false, false).req(None), Some(false)); + } + + fn test_lt(l1: T, l2: T, r: T) + where + T: Into> + Clone, + { + assert_eq!(lt(l1.clone(), r.clone()).req(None), Some(true)); + assert_eq!(lt(l2.clone(), r.clone()).req(None), Some(false)); + assert!(lt(l1.clone(), r.clone()).resp(None)); + assert!(!lt(l2.clone(), r.clone()).resp(None)); + assert!(lt(l1.clone(), r.clone()).resp_event(None)); + assert!(!lt(l2.clone(), r.clone()).resp_event(None)); + assert!(lt(l1.clone(), r.clone()).error(None)); + assert!(!lt(l2.clone(), r.clone()).error(None)); + assert!(lt(l1.clone(), r.clone()).field(None)); + assert!(!lt(l2.clone(), r.clone()).field(None)); } #[test] fn test_condition_not() { - assert!(Condition::::Not(Box::new(Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(2i64.into()), - ]))) - .evaluate_response(&None)); - - assert!(!Condition::::Not(Box::new(Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(1i64.into()), - ]))) - .evaluate_response(&None)); - assert!(Condition::::Not(Box::new(Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(2i64.into()), - ]))) - .evaluate_event_response(&None, &Context::new())); - - assert!(!Condition::::Not(Box::new(Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(1i64.into()), - ]))) - .evaluate_event_response(&None, &Context::new())); + assert_eq!(not(eq(1, 2)).req(None), Some(true)); + assert_eq!(not(eq(1, 1)).req(None), Some(false)); + assert!(not(eq(1, 2)).resp(None)); + assert!(!not(eq(1, 1)).resp(None)); + assert!(not(eq(1, 2)).resp_event(None)); + assert!(!not(eq(1, 1)).resp_event(None)); + assert!(not(eq(1, 2)).error(None)); + assert!(!not(eq(1, 1)).error(None)); + assert!(not(eq(1, 2)).field(None)); + assert!(!not(eq(1, 1)).field(None)); } #[test] fn test_condition_all() { - assert!(Condition::::All(vec![ - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(1i64.into()), - ]), - Condition::Eq([ - SelectorOrValue::Value(2i64.into()), - SelectorOrValue::Value(2i64.into()), - ]) - ]) - .evaluate_response(&None)); - assert!(Condition::::All(vec![ - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(1i64.into()), - ]), - Condition::Eq([ - SelectorOrValue::Value(2i64.into()), - SelectorOrValue::Value(2i64.into()), - ]) - ]) - .evaluate_event_response(&None, &Context::new())); - - let mut condition = Condition::::All(vec![ - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Selector(TestSelectorReqRes::Req), - ]), - Condition::Eq([ - SelectorOrValue::Value(3i64.into()), - SelectorOrValue::Selector(TestSelectorReqRes::Resp), - ]), - ]); - - assert!(condition.evaluate_request(&Some(1i64)).is_none()); - assert!(condition.evaluate_response(&Some(3i64))); - assert!(condition.evaluate_event_response(&Some(3i64), &Context::new())); - - let mut condition = Condition::::All(vec![ - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Selector(TestSelectorReqRes::Req), - ]), - Condition::Eq([ - SelectorOrValue::Value(3i64.into()), - SelectorOrValue::Selector(TestSelectorReqRes::Resp), - ]), - ]); - - assert!(condition.evaluate_request(&Some(1i64)).is_none()); - assert!(!condition.evaluate_response(&Some(2i64))); - assert!(!condition.evaluate_event_response(&Some(2i64), &Context::new())); - - let mut condition = Condition::::All(vec![ - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Selector(TestSelectorReqRes::Req), - ]), - Condition::Eq([ - SelectorOrValue::Value(3i64.into()), - SelectorOrValue::Selector(TestSelectorReqRes::Req), - ]), - ]); - - assert_eq!(Some(false), condition.evaluate_request(&Some(1i64))); - assert!(!condition.evaluate_response(&Some(2i64))); - assert!(!condition.evaluate_event_response(&Some(2i64), &Context::new())); - - assert!(!Condition::::All(vec![ - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(1i64.into()), - ]), - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(2i64.into()), - ]) - ]) - .evaluate_response(&None)); - - assert!(!Condition::::All(vec![ - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(1i64.into()), - ]), - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(2i64.into()), - ]) - ]) - .evaluate_event_response(&None, &Context::new())); + assert_eq!(all(eq(1, 1), eq(1, 2)).req(None), Some(false)); + assert_eq!(all(eq(1, 1), eq(2, 2)).req(None), Some(true)); + assert!(!all(eq(1, 1), eq(1, 2)).resp(None)); + assert!(all(eq(1, 1), eq(2, 2)).resp(None)); + assert!(!all(eq(1, 1), eq(1, 2)).resp_event(None)); + assert!(all(eq(1, 1), eq(2, 2)).resp_event(None)); + assert!(!all(eq(1, 1), eq(1, 2)).error(None)); + assert!(all(eq(1, 1), eq(2, 2)).error(None)); + assert!(!all(eq(1, 1), eq(1, 2)).field(None)); + assert!(all(eq(1, 1), eq(2, 2)).field(None)); } #[test] fn test_condition_any() { - assert!(Condition::::Any(vec![ - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(1i64.into()), - ]), - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(2i64.into()), - ]) - ]) - .evaluate_response(&None)); - - assert!(!Condition::::All(vec![ - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(2i64.into()), - ]), - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(2i64.into()), - ]) - ]) - .evaluate_response(&None)); - assert!(Condition::::Any(vec![ - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(1i64.into()), - ]), - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(2i64.into()), - ]) - ]) - .evaluate_event_response(&None, &Context::new())); - - assert!(!Condition::::All(vec![ - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(2i64.into()), - ]), - Condition::Eq([ - SelectorOrValue::Value(1i64.into()), - SelectorOrValue::Value(2i64.into()), - ]) - ]) - .evaluate_event_response(&None, &Context::new())); + assert_eq!(any(eq(1, 1), eq(1, 2)).req(None), Some(true)); + assert_eq!(any(eq(1, 1), eq(2, 2)).req(None), Some(true)); + assert!(any(eq(1, 1), eq(1, 2)).resp(None)); + assert!(any(eq(1, 1), eq(2, 2)).resp(None)); + assert!(any(eq(1, 1), eq(1, 2)).resp_event(None)); + assert!(any(eq(1, 1), eq(2, 2)).resp_event(None)); + assert!(any(eq(1, 1), eq(1, 2)).error(None)); + assert!(any(eq(1, 1), eq(2, 2)).error(None)); + assert!(any(eq(1, 1), eq(1, 2)).field(None)); + assert!(any(eq(1, 1), eq(2, 2)).field(None)); + } + + #[test] + fn test_rewrite() { + // These conditions are stateful and require that the request first evaluated before the response will yield true. + let mut condition = all(eq(1, Req), eq(3, Resp)); + assert!(!condition.resp(Some(3i64))); + assert!(condition.req(Some(1i64)).is_none()); + assert!(condition.resp(Some(3i64))); + + let mut condition = all(gt(2, Req), eq(3, Resp)); + assert!(!condition.resp(Some(3i64))); + assert!(condition.req(Some(1i64)).is_none()); + assert!(condition.resp(Some(3i64))); + + let mut condition = all(lt(1, Req), eq(3, Resp)); + assert!(!condition.resp(Some(3i64))); + assert!(condition.req(Some(2i64)).is_none()); + assert!(condition.resp(Some(3i64))); + + let mut condition = all(exists(Req), eq(3, Resp)); + assert!(!condition.resp(Some(3i64))); + assert!(condition.req(Some(1i64)).is_none()); + assert!(condition.resp(Some(3i64))); + } + + #[test] + fn test_condition_selector() { + // req handling is special so needs extensive testing. Other methods are just passthoughs, so we can so a single check on eq + assert_eq!(eq(Req, 1).req(Some(1i64)), Some(true)); + assert_eq!(eq(Req, 1).req(None), None); + assert_eq!(eq(1, Req).req(Some(1i64)), Some(true)); + assert_eq!(eq(1, Req).req(None), None); + assert_eq!(eq(Req, Req).req(Some(1i64)), Some(true)); + assert_eq!(eq(Req, Req).req(None), None); + + assert_eq!(gt(Req, 1).req(Some(2i64)), Some(true)); + assert_eq!(gt(Req, 1).req(None), None); + assert_eq!(gt(2, Req).req(Some(1i64)), Some(true)); + assert_eq!(gt(2, Req).req(None), None); + assert_eq!(gt(Req, Req).req(Some(1i64)), Some(false)); + assert_eq!(gt(Req, 1).req(None), None); + + assert_eq!(lt(Req, 2).req(Some(1i64)), Some(true)); + assert_eq!(lt(Req, 2).req(None), None); + assert_eq!(lt(1, Req).req(Some(2i64)), Some(true)); + assert_eq!(lt(1, Req).req(None), None); + assert_eq!(lt(Req, Req).req(Some(1i64)), Some(false)); + assert_eq!(lt(Req, Req).req(None), None); + + assert_eq!(exists(Req).req(Some(1i64)), Some(true)); + assert_eq!(exists(Req).req(None), None); + + assert_eq!(all(eq(1, 1), eq(1, Req)).req(Some(1i64)), Some(true)); + assert_eq!(all(eq(1, 1), eq(1, Req)).req(None), None); + assert_eq!(any(eq(1, 2), eq(1, Req)).req(Some(1i64)), Some(true)); + assert_eq!(any(eq(1, 2), eq(1, Req)).req(None), None); + + assert!(eq(Resp, 1).resp(Some(1i64))); + assert!(eq(Resp, 1).resp_event(Some(1i64))); + assert!(eq(Resp, 1).field(Some(1i64))); + assert!(eq(Resp, "error").error(Some("error"))); + } + + #[test] + fn test_evaluate_drop() { + assert!(eq(Req, 1).evaluate_drop().is_none()); + assert!(eq(1, Req).evaluate_drop().is_none()); + assert_eq!(eq(1, 1).evaluate_drop(), Some(true)); + assert_eq!(eq(1, 2).evaluate_drop(), Some(false)); + assert_eq!(eq(Static(1), 1).evaluate_drop(), Some(true)); + assert_eq!(eq(1, Static(2)).evaluate_drop(), Some(false)); + assert_eq!(lt(1, 2).evaluate_drop(), Some(true)); + assert_eq!(lt(2, 1).evaluate_drop(), Some(false)); + assert_eq!(lt(Static(1), 2).evaluate_drop(), Some(true)); + assert_eq!(lt(2, Static(1)).evaluate_drop(), Some(false)); + assert_eq!(gt(2, 1).evaluate_drop(), Some(true)); + assert_eq!(gt(1, 2).evaluate_drop(), Some(false)); + assert_eq!(gt(Static(2), 1).evaluate_drop(), Some(true)); + assert_eq!(gt(1, Static(2)).evaluate_drop(), Some(false)); + assert_eq!(not(eq(1, 2)).evaluate_drop(), Some(true)); + assert_eq!(not(eq(1, 1)).evaluate_drop(), Some(false)); + assert_eq!(all(eq(1, 1), eq(2, 2)).evaluate_drop(), Some(true)); + assert_eq!(all(eq(1, 1), eq(2, 1)).evaluate_drop(), Some(false)); + assert_eq!(any(eq(1, 1), eq(1, 2)).evaluate_drop(), Some(true)); + assert_eq!(any(eq(1, 2), eq(1, 2)).evaluate_drop(), Some(false)); + } + + fn exists(selector: TestSelector) -> Condition { + Condition::::Exists(selector) + } + + fn not(selector: Condition) -> Condition { + Condition::::Not(Box::new(selector)) + } + + fn eq(left: L, right: R) -> Condition + where + L: Into>, + R: Into>, + { + Condition::::Eq([left.into(), right.into()]) + } + + fn gt(left: L, right: R) -> Condition + where + L: Into>, + R: Into>, + { + Condition::::Gt([left.into(), right.into()]) + } + + fn lt(left: L, right: R) -> Condition + where + L: Into>, + R: Into>, + { + Condition::::Lt([left.into(), right.into()]) + } + + fn all(l: Condition, r: Condition) -> Condition +where { + Condition::::All(vec![l, r]) + } + + fn any(l: Condition, r: Condition) -> Condition +where { + Condition::::Any(vec![l, r]) + } + + impl From for SelectorOrValue { + fn from(value: bool) -> Self { + SelectorOrValue::Value(value.into()) + } + } + + impl From for SelectorOrValue { + fn from(value: i64) -> Self { + SelectorOrValue::Value(value.into()) + } + } + + impl From for SelectorOrValue { + fn from(value: f64) -> Self { + SelectorOrValue::Value(value.into()) + } + } + impl From<&str> for SelectorOrValue { + fn from(value: &str) -> Self { + SelectorOrValue::Value(value.to_string().into()) + } + } + + impl From for SelectorOrValue { + fn from(value: TestSelector) -> Self { + SelectorOrValue::Selector(value) + } + } + + impl Condition { + fn req(&mut self, value: Option) -> Option { + self.evaluate_request(&value) + } + fn resp(&mut self, value: Option) -> bool { + self.evaluate_response(&value) + } + fn resp_event(&mut self, value: Option) -> bool { + self.evaluate_event_response(&value, &Context::new()) + } + fn error(&mut self, value: Option<&str>) -> bool { + self.evaluate_error(&BoxError::from(value.unwrap_or(""))) + } + fn field(&mut self, value: Option) -> bool { + match value { + None => self.evaluate_response_field( + &TypedValue::Bool(ty(), field(), &false), + &Context::new(), + ), + Some(value) => self.evaluate_response_field( + &TypedValue::Number(ty(), field(), &serde_json::Number::from(value)), + &Context::new(), + ), + } + } } } diff --git a/apollo-router/src/plugins/telemetry/config_new/extendable.rs b/apollo-router/src/plugins/telemetry/config_new/extendable.rs index 21c1333d47..eaed22c405 100644 --- a/apollo-router/src/plugins/telemetry/config_new/extendable.rs +++ b/apollo-router/src/plugins/telemetry/config_new/extendable.rs @@ -17,6 +17,7 @@ use serde_json::Map; use serde_json::Value; use tower::BoxError; +use crate::plugins::demand_control::cost_calculator::schema_aware_response::TypedValue; use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel; use crate::plugins::telemetry::config_new::DefaultForLevel; use crate::plugins::telemetry::config_new::Selector; @@ -228,6 +229,18 @@ where attrs } + + fn on_response_field(&self, typed_value: &TypedValue, ctx: &Context) -> Vec { + let mut attrs = self.attributes.on_response_field(typed_value, ctx); + let custom_attributes = self.custom.iter().filter_map(|(key, value)| { + value + .on_response_field(typed_value, ctx) + .map(|v| KeyValue::new(key.clone(), v)) + }); + attrs.extend(custom_attributes); + + attrs + } } #[cfg(test)] diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter/metrics.snap new file mode 100644 index 0000000000..a9deebe733 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter/metrics.snap @@ -0,0 +1,22 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + graphql: + custom_counter: + description: count of requests + type: counter + unit: unit + value: field_unit +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 2 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter/router.yaml new file mode 100644 index 0000000000..1e2be58fbb --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter/router.yaml @@ -0,0 +1,9 @@ +telemetry: + instrumentation: + instruments: + graphql: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: field_unit diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter/test.yaml new file mode 100644 index 0000000000..7fd37b474b --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter/test.yaml @@ -0,0 +1,31 @@ +description: Custom counter +events: + - - response_field: + typed_value: + string: + value: "product1" + field_name: "name" + field_type: "String" + type_name: "Product" + - response_field: + typed_value: + list: + values: + - string: + value: "product2" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product3" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product4" + type_name: "Product" + field_type: "String" + field_name: "name" + field_name: "products" + field_type: "String" + type_name: "Query" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_attributes/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_attributes/metrics.snap new file mode 100644 index 0000000000..90c37944f7 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_attributes/metrics.snap @@ -0,0 +1,38 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + graphql: + custom_counter: + description: count of requests + type: counter + unit: unit + value: field_unit + attributes: + graphql.field.name: true + graphql.field.type: true + graphql.type.name: true + custom_attribute: + field_name: string +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: + custom_attribute: name + graphql.field.name: name + graphql.field.type: String + graphql.type.name: Product + - value: 1 + attributes: + custom_attribute: products + graphql.field.name: products + graphql.field.type: String + graphql.type.name: Query diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_attributes/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_attributes/router.yaml new file mode 100644 index 0000000000..bf50e27632 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_attributes/router.yaml @@ -0,0 +1,16 @@ +telemetry: + instrumentation: + instruments: + graphql: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: field_unit + attributes: + graphql.field.name: true + graphql.field.type: true + graphql.type.name: true + "custom_attribute": + field_name: string + diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_attributes/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_attributes/test.yaml new file mode 100644 index 0000000000..7887c1f059 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_attributes/test.yaml @@ -0,0 +1,31 @@ +description: Custom counter with attributes +events: + - - response_field: + typed_value: + string: + value: "product1" + field_name: "name" + field_type: "String" + type_name: "Product" + - response_field: + typed_value: + list: + values: + - string: + value: "product2" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product3" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product4" + type_name: "Product" + field_type: "String" + field_name: "name" + field_name: "products" + field_type: "String" + type_name: "Query" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_conditions/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_conditions/metrics.snap new file mode 100644 index 0000000000..b57199486d --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_conditions/metrics.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter with conditions +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + graphql: + custom_counter: + description: count of requests + type: counter + unit: unit + value: field_unit + attributes: + graphql.field.name: true + condition: + eq: + - field_name: string + - static: products +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: + graphql.field.name: products diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_conditions/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_conditions/router.yaml new file mode 100644 index 0000000000..643bf75a0b --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_conditions/router.yaml @@ -0,0 +1,16 @@ +telemetry: + instrumentation: + instruments: + graphql: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: field_unit + attributes: + graphql.field.name: true + condition: + eq: + - field_name: string + - static: "products" + diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_conditions/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_conditions/test.yaml new file mode 100644 index 0000000000..012e574ac1 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_conditions/test.yaml @@ -0,0 +1,31 @@ +description: Custom counter with conditions +events: + - - response_field: + typed_value: + string: + value: "product1" + field_name: "name" + field_type: "String" + type_name: "Product" + - response_field: + typed_value: + list: + values: + - string: + value: "product2" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product3" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product4" + type_name: "Product" + field_type: "String" + field_name: "name" + field_name: "products" + field_type: "String" + type_name: "Query" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_value/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_value/metrics.snap new file mode 100644 index 0000000000..d4b971fef7 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_value/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + graphql: + custom_counter: + description: count of requests + type: counter + unit: unit + value: + field_custom: + list_length: value +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 3 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_value/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_value/router.yaml new file mode 100644 index 0000000000..f893c5b337 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_value/router.yaml @@ -0,0 +1,11 @@ +telemetry: + instrumentation: + instruments: + graphql: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: + field_custom: + list_length: value diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_value/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_value/test.yaml new file mode 100644 index 0000000000..7fd37b474b --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_counter_with_custom_value/test.yaml @@ -0,0 +1,31 @@ +description: Custom counter +events: + - - response_field: + typed_value: + string: + value: "product1" + field_name: "name" + field_type: "String" + type_name: "Product" + - response_field: + typed_value: + list: + values: + - string: + value: "product2" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product3" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product4" + type_name: "Product" + field_type: "String" + field_name: "name" + field_name: "products" + field_type: "String" + type_name: "Query" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram/metrics.snap new file mode 100644 index 0000000000..0b139624ac --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + graphql: + custom_counter: + description: count of requests + type: histogram + unit: unit + value: + field_custom: + list_length: value +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - sum: 5 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram/router.yaml new file mode 100644 index 0000000000..8817c3a8e0 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram/router.yaml @@ -0,0 +1,12 @@ +telemetry: + instrumentation: + instruments: + graphql: + "custom_counter": + description: "count of requests" + type: histogram + unit: "unit" + value: + field_custom: + list_length: value + diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram/test.yaml new file mode 100644 index 0000000000..c5fdea6092 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram/test.yaml @@ -0,0 +1,41 @@ +description: Custom counter +events: + - - response_field: + typed_value: + list: + values: + - string: + value: "product2" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product3" + type_name: "Product" + field_type: "String" + field_name: "name" + field_name: "products" + field_type: "String" + type_name: "Query" + - response_field: + typed_value: + list: + values: + - string: + value: "product2" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product3" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product4" + type_name: "Product" + field_type: "String" + field_name: "name" + field_name: "products" + field_type: "String" + type_name: "Query" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_attributes/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_attributes/metrics.snap new file mode 100644 index 0000000000..0896c79866 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_attributes/metrics.snap @@ -0,0 +1,38 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter with attributes +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + graphql: + custom_counter: + description: count of requests + type: histogram + unit: unit + value: field_unit + attributes: + graphql.field.name: true + graphql.field.type: true + graphql.type.name: true + custom_attribute: + field_name: string +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: + custom_attribute: name + graphql.field.name: name + graphql.field.type: String + graphql.type.name: Product + - sum: 1 + attributes: + custom_attribute: products + graphql.field.name: products + graphql.field.type: String + graphql.type.name: Query diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_attributes/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_attributes/router.yaml new file mode 100644 index 0000000000..1c60f4d809 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_attributes/router.yaml @@ -0,0 +1,16 @@ +telemetry: + instrumentation: + instruments: + graphql: + "custom_counter": + description: "count of requests" + type: histogram + unit: "unit" + value: field_unit + attributes: + graphql.field.name: true + graphql.field.type: true + graphql.type.name: true + "custom_attribute": + field_name: string + diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_attributes/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_attributes/test.yaml new file mode 100644 index 0000000000..7887c1f059 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_attributes/test.yaml @@ -0,0 +1,31 @@ +description: Custom counter with attributes +events: + - - response_field: + typed_value: + string: + value: "product1" + field_name: "name" + field_type: "String" + type_name: "Product" + - response_field: + typed_value: + list: + values: + - string: + value: "product2" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product3" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product4" + type_name: "Product" + field_type: "String" + field_name: "name" + field_name: "products" + field_type: "String" + type_name: "Query" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_conditions/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_conditions/metrics.snap new file mode 100644 index 0000000000..442a93cd6c --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_conditions/metrics.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter with conditions +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + graphql: + custom_counter: + description: count of requests + type: histogram + unit: unit + value: field_unit + attributes: + graphql.field.name: true + condition: + eq: + - field_name: string + - static: products +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: + graphql.field.name: products diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_conditions/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_conditions/router.yaml new file mode 100644 index 0000000000..1282aedbf8 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_conditions/router.yaml @@ -0,0 +1,16 @@ +telemetry: + instrumentation: + instruments: + graphql: + "custom_counter": + description: "count of requests" + type: histogram + unit: "unit" + value: field_unit + attributes: + graphql.field.name: true + condition: + eq: + - field_name: string + - static: "products" + diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_conditions/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_conditions/test.yaml new file mode 100644 index 0000000000..012e574ac1 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/custom_histogram_with_custom_conditions/test.yaml @@ -0,0 +1,31 @@ +description: Custom counter with conditions +events: + - - response_field: + typed_value: + string: + value: "product1" + field_name: "name" + field_type: "String" + type_name: "Product" + - response_field: + typed_value: + list: + values: + - string: + value: "product2" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product3" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product4" + type_name: "Product" + field_type: "String" + field_name: "name" + field_name: "products" + field_type: "String" + type_name: "Query" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.execution/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.execution/metrics.snap new file mode 100644 index 0000000000..d1214d2f8f --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.execution/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Field execution +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + graphql: + field.execution: true +--- +- name: graphql.field.execution + data: + datapoints: + - value: 1 + attributes: + graphql.field.name: name + graphql.field.type: String + graphql.type.name: Product + - value: 1 + attributes: + graphql.field.name: products + graphql.field.type: String + graphql.type.name: Query diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.execution/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.execution/router.yaml new file mode 100644 index 0000000000..ff7599fe16 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.execution/router.yaml @@ -0,0 +1,5 @@ +telemetry: + instrumentation: + instruments: + graphql: + field.execution: true \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.execution/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.execution/test.yaml new file mode 100644 index 0000000000..a1546538c4 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.execution/test.yaml @@ -0,0 +1,31 @@ +description: Field execution +events: + - - response_field: + typed_value: + string: + value: "product1" + field_name: "name" + field_type: "String" + type_name: "Product" + - response_field: + typed_value: + list: + values: + - string: + value: "product2" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product3" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product4" + type_name: "Product" + field_type: "String" + field_name: "name" + field_name: "products" + field_type: "String" + type_name: "Query" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.length/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.length/metrics.snap new file mode 100644 index 0000000000..cce830a861 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.length/metrics.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Field length +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + graphql: + list.length: true +--- +- name: graphql.field.list.length + data: + datapoints: + - sum: 3 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.length/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.length/router.yaml new file mode 100644 index 0000000000..cfb71858a9 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.length/router.yaml @@ -0,0 +1,6 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + graphql: + list.length: true \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.length/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.length/test.yaml new file mode 100644 index 0000000000..838574e6c8 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql/field.length/test.yaml @@ -0,0 +1,31 @@ +description: Field length +events: + - - response_field: + typed_value: + string: + value: "product1" + field_name: "name" + field_type: "String" + type_name: "Product" + - response_field: + typed_value: + list: + values: + - string: + value: "product2" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product3" + type_name: "Product" + field_type: "String" + field_name: "name" + - string: + value: "product4" + type_name: "Product" + field_type: "String" + field_name: "name" + field_name: "products" + field_type: "Product" + type_name: "Query" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql_instruments.router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql_instruments.router.yaml new file mode 100644 index 0000000000..df09a6b87b --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/graphql_instruments.router.yaml @@ -0,0 +1,44 @@ +telemetry: + exporters: + tracing: + propagation: + trace_context: true + metrics: + prometheus: + enabled: true + instrumentation: + instruments: + graphql: + field.execution: true + list.length: true + "custom_counter": + description: "count of name field" + type: counter + unit: "unit" + value: field_unit + attributes: + graphql.type.name: true + graphql.field.type: true + graphql.field.name: true + condition: + eq: + - field_name: string + - "name" + "custom.histogram": + description: "histogram of review length" + type: histogram + unit: "unit" + attributes: + graphql.type.name: true + graphql.field.type: true + graphql.field.name: true + value: + field_custom: + list_length: value + condition: + eq: + - field_name: string + - "topProducts" + + + diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.error.type/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.error.type/metrics.snap new file mode 100644 index 0000000000..1f36a6f93d --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.error.type/metrics.snap @@ -0,0 +1,20 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + router: + http.server.request.duration: + attributes: + error.type: true +--- +- name: http.server.request.duration + data: + datapoints: + - sum: 0.1 + attributes: + error.type: Internal Server Error diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.error.type/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.error.type/router.yaml new file mode 100644 index 0000000000..0db5303269 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.error.type/router.yaml @@ -0,0 +1,8 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + router: + http.server.request.duration: + attributes: + error.type: true diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.error.type/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.error.type/test.yaml new file mode 100644 index 0000000000..fee5ee56da --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.error.type/test.yaml @@ -0,0 +1,9 @@ +description: error.type attribute +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - router_error: + error: "router error" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.on_graphql_error/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.on_graphql_error/metrics.snap new file mode 100644 index 0000000000..b3d6df9afd --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.on_graphql_error/metrics.snap @@ -0,0 +1,21 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: on_graphql_error attribute +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + router: + http.server.request.duration: + attributes: + on.graphql.error: + on_graphql_error: true +--- +- name: http.server.request.duration + data: + datapoints: + - sum: 0.1 + attributes: + on.graphql.error: true diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.on_graphql_error/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.on_graphql_error/router.yaml new file mode 100644 index 0000000000..263de4a50c --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.on_graphql_error/router.yaml @@ -0,0 +1,9 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + router: + http.server.request.duration: + attributes: + on.graphql.error: + on_graphql_error: true diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.on_graphql_error/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.on_graphql_error/test.yaml new file mode 100644 index 0000000000..c61052951f --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/attribute.on_graphql_error/test.yaml @@ -0,0 +1,15 @@ +description: on_graphql_error attribute +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - context: + map: + "apollo::telemetry::contains_graphql_error": true + # This feels like a bug, on drop should still generate an error. + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter/metrics.snap new file mode 100644 index 0000000000..919d30bb62 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter/router.yaml new file mode 100644 index 0000000000..dbecbd4ed6 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter/router.yaml @@ -0,0 +1,11 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter/test.yaml new file mode 100644 index 0000000000..7be5b15916 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter/test.yaml @@ -0,0 +1,11 @@ +description: Custom counter +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request/metrics.snap new file mode 100644 index 0000000000..919d30bb62 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request/router.yaml new file mode 100644 index 0000000000..dbecbd4ed6 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request/router.yaml @@ -0,0 +1,11 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request/test.yaml new file mode 100644 index 0000000000..41e6b0a252 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request/test.yaml @@ -0,0 +1,7 @@ +description: Custom counter where the router response does not happen. This should still increment the metric on Drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_request/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_request/metrics.snap new file mode 100644 index 0000000000..6dd2249831 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_request/metrics.snap @@ -0,0 +1,28 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter should not be incremented as the condition is not true on drop. +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit + condition: + eq: + - request_header: always-true + - static: "true" +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_request/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_request/router.yaml new file mode 100644 index 0000000000..b0fb1ba36d --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_request/router.yaml @@ -0,0 +1,16 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit + # This instrument should not be triggered as the condition is never true + condition: + eq: + - request_header: "always-true" + - static: "true" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_request/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_request/test.yaml new file mode 100644 index 0000000000..7a840fc5fb --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_request/test.yaml @@ -0,0 +1,10 @@ +description: Custom counter should not be incremented as the condition is not true on drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + headers: + always-true: "true" + diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_response/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_response/metrics.snap new file mode 100644 index 0000000000..7c2ac60d91 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_response/metrics.snap @@ -0,0 +1,22 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit + condition: + eq: + - response_header: never-true + - static: "true" +--- +[] diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_response/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_response/router.yaml new file mode 100644 index 0000000000..5a8f5bdb14 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_response/router.yaml @@ -0,0 +1,16 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit + # This instrument should not be triggered as the condition is never true + condition: + eq: + - response_header: "never-true" + - "true" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_response/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_response/test.yaml new file mode 100644 index 0000000000..ae21fbe5d8 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_aborted_request_with_condition_on_response/test.yaml @@ -0,0 +1,7 @@ +description: Custom counter should not be incremented as the condition is not true on drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_custom_value/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_custom_value/metrics.snap new file mode 100644 index 0000000000..42f4e68f83 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_custom_value/metrics.snap @@ -0,0 +1,25 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: + request_header: count_header +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 10 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_custom_value/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_custom_value/router.yaml new file mode 100644 index 0000000000..f6a40d1765 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_custom_value/router.yaml @@ -0,0 +1,12 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: + request_header: "count_header" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_custom_value/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_custom_value/test.yaml new file mode 100644 index 0000000000..6579360555 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_custom_value/test.yaml @@ -0,0 +1,13 @@ +description: Custom counter that gets a value from a header +events: + - - router_request: + uri: "/hello" + method: GET + headers: + count_header: 10 + body: | + hello + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_attributes/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_attributes/metrics.snap new file mode 100644 index 0000000000..c0c947c3d5 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_attributes/metrics.snap @@ -0,0 +1,30 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit + attributes: + http.request.method: true + custom_attribute: + request_header: custom_header +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: + custom_attribute: custom_value + http.request.method: GET diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_attributes/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_attributes/router.yaml new file mode 100644 index 0000000000..ae6977491d --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_attributes/router.yaml @@ -0,0 +1,15 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit + attributes: + http.request.method: true + "custom_attribute": + request_header: "custom_header" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_attributes/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_attributes/test.yaml new file mode 100644 index 0000000000..09a4244828 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_attributes/test.yaml @@ -0,0 +1,13 @@ +description: Custom counter with attributes +events: + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + body: | + hello + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_conditions/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_conditions/metrics.snap new file mode 100644 index 0000000000..482f591751 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_conditions/metrics.snap @@ -0,0 +1,34 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter with conditions +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit + attributes: + http.request.method: true + custom_attribute: + request_header: custom_header + condition: + eq: + - request_header: custom_header + - allowed +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: + custom_attribute: allowed + http.request.method: GET diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_conditions/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_conditions/router.yaml new file mode 100644 index 0000000000..5e1bdc25ca --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_conditions/router.yaml @@ -0,0 +1,19 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit + attributes: + http.request.method: true + "custom_attribute": + request_header: "custom_header" + condition: + eq: + - request_header: "custom_header" + - "allowed" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_conditions/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_conditions/test.yaml new file mode 100644 index 0000000000..e97ad55096 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_counter_with_conditions/test.yaml @@ -0,0 +1,25 @@ +description: Custom counter with conditions +events: + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: allowed + body: | + hello + - router_response: + body: | + hello + status: 200 + + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: not_allowed + body: | + hello + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_custom_value/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_custom_value/metrics.snap new file mode 100644 index 0000000000..957fec0e25 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_custom_value/metrics.snap @@ -0,0 +1,25 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram with value from custom header +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: + request_header: count_header +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 10 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_custom_value/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_custom_value/router.yaml new file mode 100644 index 0000000000..74d95b1924 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_custom_value/router.yaml @@ -0,0 +1,12 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: + request_header: "count_header" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_custom_value/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_custom_value/test.yaml new file mode 100644 index 0000000000..d3869627e7 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_custom_value/test.yaml @@ -0,0 +1,13 @@ +description: Custom histogram with value from custom header +events: + - - router_request: + uri: "/hello" + method: GET + headers: + count_header: "10" + body: | + hello + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration/metrics.snap new file mode 100644 index 0000000000..815e578506 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram duration +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram.duration: + description: histogram of requests + type: histogram + unit: unit + value: duration +--- +- name: custom.histogram.duration + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 0.1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration/router.yaml new file mode 100644 index 0000000000..e1796e90b6 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration/router.yaml @@ -0,0 +1,11 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom.histogram.duration": + description: "histogram of requests" + type: histogram + unit: "unit" + value: duration \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration/test.yaml new file mode 100644 index 0000000000..8b50a936a7 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration/test.yaml @@ -0,0 +1,11 @@ +description: Custom histogram duration +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request/metrics.snap new file mode 100644 index 0000000000..a01097bae5 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: "Custom histogram where router response doesn't happen. This should still increment the metric on Drop." +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram.duration: + description: histogram of requests + type: histogram + unit: unit + value: duration +--- +- name: custom.histogram.duration + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 0.1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request/router.yaml new file mode 100644 index 0000000000..e1796e90b6 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request/router.yaml @@ -0,0 +1,11 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom.histogram.duration": + description: "histogram of requests" + type: histogram + unit: "unit" + value: duration \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request/test.yaml new file mode 100644 index 0000000000..1ab9356053 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request/test.yaml @@ -0,0 +1,7 @@ +description: Custom histogram where router response doesn't happen. This should still increment the metric on Drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_request/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_request/metrics.snap new file mode 100644 index 0000000000..54cb6228f9 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_request/metrics.snap @@ -0,0 +1,28 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram should not be incremented as the condition is not true on drop. +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram.duration: + description: histogram of requests + type: histogram + unit: unit + value: duration + condition: + eq: + - request_header: always-true + - static: "true" +--- +- name: custom.histogram.duration + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 0.1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_request/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_request/router.yaml new file mode 100644 index 0000000000..1069fcc082 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_request/router.yaml @@ -0,0 +1,16 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom.histogram.duration": + description: "histogram of requests" + type: histogram + unit: "unit" + value: duration + # This instrument should be triggered on drop as the condition is true + condition: + eq: + - request_header: "always-true" + - static: "true" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_request/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_request/test.yaml new file mode 100644 index 0000000000..bda4a846a4 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_request/test.yaml @@ -0,0 +1,9 @@ +description: Custom histogram should not be incremented as the condition is not true on drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + headers: + always-true: "true" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_response/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_response/metrics.snap new file mode 100644 index 0000000000..5c06e1143e --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_response/metrics.snap @@ -0,0 +1,22 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram should not be incremented as the condition is not true on drop. +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram.duration: + description: histogram of requests + type: histogram + unit: unit + value: duration + condition: + eq: + - response_header: never-true + - static: "true" +--- +[] diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_response/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_response/router.yaml new file mode 100644 index 0000000000..36048c0c55 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_response/router.yaml @@ -0,0 +1,16 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom.histogram.duration": + description: "histogram of requests" + type: histogram + unit: "unit" + value: duration + # This instrument should not be triggered on drop as the condition is never true + condition: + eq: + - response_header: "never-true" + - "true" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_response/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_response/test.yaml new file mode 100644 index 0000000000..89a7ad610b --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_duration_aborted_request_with_condition_on_response/test.yaml @@ -0,0 +1,7 @@ +description: Custom histogram should not be incremented as the condition is not true on drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit/metrics.snap new file mode 100644 index 0000000000..5d4f3f4e4e --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram unit +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit/router.yaml new file mode 100644 index 0000000000..813abb4615 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit/router.yaml @@ -0,0 +1,11 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit/test.yaml new file mode 100644 index 0000000000..84fe9ed7ae --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit/test.yaml @@ -0,0 +1,11 @@ +description: Custom histogram unit +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request/metrics.snap new file mode 100644 index 0000000000..5d4f3f4e4e --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram unit +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request/router.yaml new file mode 100644 index 0000000000..813abb4615 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request/router.yaml @@ -0,0 +1,11 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request/test.yaml new file mode 100644 index 0000000000..1ab9356053 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request/test.yaml @@ -0,0 +1,7 @@ +description: Custom histogram where router response doesn't happen. This should still increment the metric on Drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_request/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_request/metrics.snap new file mode 100644 index 0000000000..c32ce4a968 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_request/metrics.snap @@ -0,0 +1,28 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram should not be incremented as the condition is not true on drop. +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit + condition: + eq: + - request_header: always-true + - static: "true" +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_request/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_request/router.yaml new file mode 100644 index 0000000000..3df5d20913 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_request/router.yaml @@ -0,0 +1,16 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit + # This instrument should be triggered on drop as the condition is true + condition: + eq: + - request_header: "always-true" + - static: "true" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_request/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_request/test.yaml new file mode 100644 index 0000000000..bda4a846a4 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_request/test.yaml @@ -0,0 +1,9 @@ +description: Custom histogram should not be incremented as the condition is not true on drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + headers: + always-true: "true" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_response/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_response/metrics.snap new file mode 100644 index 0000000000..70038f2490 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_response/metrics.snap @@ -0,0 +1,22 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram should not be incremented as the condition is not true on drop. +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit + condition: + eq: + - response_header: never-true + - static: "true" +--- +[] diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_response/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_response/router.yaml new file mode 100644 index 0000000000..dfefa155d1 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_response/router.yaml @@ -0,0 +1,16 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit + # This instrument should not be triggered on drop as the condition is never true + condition: + eq: + - response_header: "never-true" + - "true" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_response/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_response/test.yaml new file mode 100644 index 0000000000..89a7ad610b --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_unit_aborted_request_with_condition_on_response/test.yaml @@ -0,0 +1,7 @@ +description: Custom histogram should not be incremented as the condition is not true on drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_attributes/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_attributes/metrics.snap new file mode 100644 index 0000000000..66de37a22b --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_attributes/metrics.snap @@ -0,0 +1,30 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram with attributes +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit + attributes: + http.request.method: true + custom_attribute: + request_header: custom_header +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: + custom_attribute: custom_value + http.request.method: GET diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_attributes/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_attributes/router.yaml new file mode 100644 index 0000000000..d37b70d822 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_attributes/router.yaml @@ -0,0 +1,15 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit + attributes: + http.request.method: true + "custom_attribute": + request_header: "custom_header" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_attributes/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_attributes/test.yaml new file mode 100644 index 0000000000..2744185378 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_attributes/test.yaml @@ -0,0 +1,13 @@ +description: Custom histogram with attributes +events: + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: "custom_value" + body: | + hello + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_conditions/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_conditions/metrics.snap new file mode 100644 index 0000000000..5377b7f289 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_conditions/metrics.snap @@ -0,0 +1,34 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter with conditions +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit + attributes: + http.request.method: true + custom_attribute: + request_header: custom_header + condition: + eq: + - request_header: custom_header + - allowed +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: + custom_attribute: allowed + http.request.method: GET diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_conditions/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_conditions/router.yaml new file mode 100644 index 0000000000..faa6a69a28 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_conditions/router.yaml @@ -0,0 +1,19 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit + attributes: + http.request.method: true + "custom_attribute": + request_header: "custom_header" + condition: + eq: + - request_header: "custom_header" + - "allowed" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_conditions/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_conditions/test.yaml new file mode 100644 index 0000000000..e97ad55096 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/custom_histogram_with_conditions/test.yaml @@ -0,0 +1,25 @@ +description: Custom counter with conditions +events: + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: allowed + body: | + hello + - router_response: + body: | + hello + status: 200 + + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: not_allowed + body: | + hello + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.active_requests/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.active_requests/metrics.snap new file mode 100644 index 0000000000..762e9b3950 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.active_requests/metrics.snap @@ -0,0 +1,18 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test standard router metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: true + http.server.request.duration: false +--- +- name: http.server.active_requests + data: + datapoints: + - value: 0 + attributes: + http.request.method: GET diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.active_requests/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.active_requests/router.yaml new file mode 100644 index 0000000000..e9b3ac1c2d --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.active_requests/router.yaml @@ -0,0 +1,8 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: true + http.server.request.duration: false + subgraph: + http.client.request.duration: false \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.active_requests/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.active_requests/test.yaml new file mode 100644 index 0000000000..d4c3051957 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.active_requests/test.yaml @@ -0,0 +1,11 @@ +description: Active requests metrics +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size/metrics.snap new file mode 100644 index 0000000000..489baf58f9 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size/metrics.snap @@ -0,0 +1,20 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + http.server.request.body.size: true +--- +- name: http.server.request.body.size + data: + datapoints: + - sum: 35 + attributes: + http.request.method: GET + http.response.status_code: 200 diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size/router.yaml new file mode 100644 index 0000000000..42455f7354 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size/router.yaml @@ -0,0 +1,7 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + http.server.request.body.size: true \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size/test.yaml new file mode 100644 index 0000000000..1cf98e286c --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size/test.yaml @@ -0,0 +1,14 @@ +description: Server request body size metrics +events: + - - router_request: + uri: "/hello" + method: GET + headers: + "content-length": "35" + "content-type": "application/graphql" + body: | + hello + - router_response: + body: | + hello + status: 200 diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size_with_custom_attributes/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size_with_custom_attributes/metrics.snap new file mode 100644 index 0000000000..489baf58f9 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size_with_custom_attributes/metrics.snap @@ -0,0 +1,20 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + http.server.request.body.size: true +--- +- name: http.server.request.body.size + data: + datapoints: + - sum: 35 + attributes: + http.request.method: GET + http.response.status_code: 200 diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size_with_custom_attributes/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size_with_custom_attributes/router.yaml new file mode 100644 index 0000000000..42455f7354 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size_with_custom_attributes/router.yaml @@ -0,0 +1,7 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + http.server.request.body.size: true \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size_with_custom_attributes/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size_with_custom_attributes/test.yaml new file mode 100644 index 0000000000..1cf98e286c --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.body.size_with_custom_attributes/test.yaml @@ -0,0 +1,14 @@ +description: Server request body size metrics +events: + - - router_request: + uri: "/hello" + method: GET + headers: + "content-length": "35" + "content-type": "application/graphql" + body: | + hello + - router_response: + body: | + hello + status: 200 diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration/metrics.snap new file mode 100644 index 0000000000..9d6af18b30 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration/metrics.snap @@ -0,0 +1,19 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Server duration metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: true +--- +- name: http.server.request.duration + data: + datapoints: + - sum: 0.1 + attributes: + http.request.method: GET + http.response.status_code: 200 diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration/router.yaml new file mode 100644 index 0000000000..5411b485dd --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration/router.yaml @@ -0,0 +1,6 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: true \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration/test.yaml new file mode 100644 index 0000000000..e3c8e55092 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration/test.yaml @@ -0,0 +1,11 @@ +description: Server duration metrics +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/metrics.snap new file mode 100644 index 0000000000..8c7015b27c --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/metrics.snap @@ -0,0 +1,23 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Server duration metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: + attributes: + my_attribute: + request_method: true +--- +- name: http.server.request.duration + data: + datapoints: + - sum: 0.1 + attributes: + http.request.method: GET + http.response.status_code: 200 + my_attribute: GET diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/router.yaml new file mode 100644 index 0000000000..a2b1c08b9c --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/router.yaml @@ -0,0 +1,9 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: + attributes: + my_attribute: + request_method: true \ No newline at end of file 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 new file mode 100644 index 0000000000..e3c8e55092 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/test.yaml @@ -0,0 +1,11 @@ +description: Server duration metrics +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.response.body.size/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.response.body.size/metrics.snap new file mode 100644 index 0000000000..cd7e0d7c6f --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.response.body.size/metrics.snap @@ -0,0 +1,20 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + http.server.response.body.size: true +--- +- name: http.server.response.body.size + data: + datapoints: + - sum: 35 + attributes: + http.request.method: GET + http.response.status_code: 200 diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.response.body.size/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.response.body.size/router.yaml new file mode 100644 index 0000000000..c54db4d949 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.response.body.size/router.yaml @@ -0,0 +1,7 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + http.server.response.body.size: true \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.response.body.size/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.response.body.size/test.yaml new file mode 100644 index 0000000000..b0bbb9625a --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.response.body.size/test.yaml @@ -0,0 +1,13 @@ +description: Server request body size metrics +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - router_response: + headers: + "content-length": "35" + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router_instruments.router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router_instruments.router.yaml new file mode 100644 index 0000000000..b6b6220f1a --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router_instruments.router.yaml @@ -0,0 +1,52 @@ +telemetry: + instrumentation: + instruments: + router: + http.server.request.body.size: true + http.server.response.body.size: + attributes: + http.response.status_code: false + acme.my_attribute: + response_header: x-my-header + default: unknown + acme.request.on_error: + value: unit + type: counter + unit: error + description: my description + condition: + not: + eq: + - 200 + - response_status: code + attributes: + http.response.status_code: true + acme.request.on_critical_error: + value: unit + type: counter + unit: error + description: my description + condition: + eq: + - request time out + - error: reason + attributes: + http.response.status_code: true + acme.request.on_error_histo: + value: unit + type: histogram + unit: error + description: my description + condition: + not: + eq: + - 200 + - response_status: code + attributes: + http.response.status_code: true + acme.request.header_value: + value: + request_header: x-my-header-count + type: counter + description: my description + unit: nb diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/schema.json b/apollo-router/src/plugins/telemetry/config_new/fixtures/schema.json new file mode 100644 index 0000000000..cedb4fdb87 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/schema.json @@ -0,0 +1,636 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TestDefinition", + "type": "object", + "required": [ + "description", + "events" + ], + "properties": { + "description": { + "type": "string" + }, + "events": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Event" + } + } + } + }, + "additionalProperties": false, + "definitions": { + "Event": { + "oneOf": [ + { + "type": "object", + "required": [ + "context" + ], + "properties": { + "context": { + "type": "object", + "required": [ + "map" + ], + "properties": { + "map": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "router_request" + ], + "properties": { + "router_request": { + "type": "object", + "required": [ + "body", + "method", + "uri" + ], + "properties": { + "body": { + "type": "string" + }, + "headers": { + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "method": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "router_response" + ], + "properties": { + "router_response": { + "type": "object", + "required": [ + "body", + "status" + ], + "properties": { + "body": { + "type": "string" + }, + "headers": { + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "status": { + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "router_error" + ], + "properties": { + "router_error": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "supergraph_request" + ], + "properties": { + "supergraph_request": { + "type": "object", + "required": [ + "method", + "query", + "uri" + ], + "properties": { + "headers": { + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "method": { + "type": "string" + }, + "query": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "supergraph_response" + ], + "properties": { + "supergraph_response": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "data": true, + "errors": { + "default": [], + "type": "array", + "items": true + }, + "extensions": { + "default": {}, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "headers": { + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "subgraph_request" + ], + "properties": { + "subgraph_request": { + "type": "object", + "required": [ + "query", + "subgraph_name" + ], + "properties": { + "extensions": { + "default": {}, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "headers": { + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "operation_kind": { + "anyOf": [ + { + "$ref": "#/definitions/OperationKind" + }, + { + "type": "null" + } + ] + }, + "operation_name": { + "type": [ + "string", + "null" + ] + }, + "query": { + "type": "string" + }, + "subgraph_name": { + "type": "string" + }, + "variables": { + "default": {}, + "type": [ + "object", + "null" + ], + "additionalProperties": true + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "supergraph_error" + ], + "properties": { + "supergraph_error": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "subgraph_response" + ], + "properties": { + "subgraph_response": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "data": true, + "errors": { + "default": [], + "type": "array", + "items": true + }, + "extensions": { + "default": {}, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "headers": { + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "status": { + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Note that this MUST not be used without first using supergraph request event", + "type": "object", + "required": [ + "graphql_response" + ], + "properties": { + "graphql_response": { + "type": "object", + "properties": { + "data": true, + "errors": { + "default": [], + "type": "array", + "items": true + }, + "extensions": { + "default": {}, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "path": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Note that this MUST not be used without first using supergraph request event", + "type": "object", + "required": [ + "response_field" + ], + "properties": { + "response_field": { + "type": "object", + "required": [ + "typed_value" + ], + "properties": { + "typed_value": { + "$ref": "#/definitions/TypedValueMirror" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "OperationKind": { + "description": "GraphQL operation type.", + "type": "string", + "enum": [ + "query", + "mutation", + "subscription" + ] + }, + "TypedValueMirror": { + "oneOf": [ + { + "type": "string", + "enum": [ + "null" + ] + }, + { + "type": "object", + "required": [ + "bool" + ], + "properties": { + "bool": { + "type": "object", + "required": [ + "field_name", + "field_type", + "type_name", + "value" + ], + "properties": { + "field_name": { + "type": "string" + }, + "field_type": { + "type": "string" + }, + "type_name": { + "type": "string" + }, + "value": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "number" + ], + "properties": { + "number": { + "type": "object", + "required": [ + "field_name", + "field_type", + "type_name", + "value" + ], + "properties": { + "field_name": { + "type": "string" + }, + "field_type": { + "type": "string" + }, + "type_name": { + "type": "string" + }, + "value": { + "type": "number" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "string" + ], + "properties": { + "string": { + "type": "object", + "required": [ + "field_name", + "field_type", + "type_name", + "value" + ], + "properties": { + "field_name": { + "type": "string" + }, + "field_type": { + "type": "string" + }, + "type_name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "list" + ], + "properties": { + "list": { + "type": "object", + "required": [ + "field_name", + "field_type", + "type_name", + "values" + ], + "properties": { + "field_name": { + "type": "string" + }, + "field_type": { + "type": "string" + }, + "type_name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/TypedValueMirror" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "object" + ], + "properties": { + "object": { + "type": "object", + "required": [ + "field_name", + "field_type", + "type_name", + "values" + ], + "properties": { + "field_name": { + "type": "string" + }, + "field_type": { + "type": "string" + }, + "type_name": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/TypedValueMirror" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "root" + ], + "properties": { + "root": { + "type": "object", + "required": [ + "values" + ], + "properties": { + "values": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/TypedValueMirror" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } +} \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter/metrics.snap new file mode 100644 index 0000000000..919d30bb62 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter/router.yaml new file mode 100644 index 0000000000..665fa56088 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter/router.yaml @@ -0,0 +1,10 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter/test.yaml new file mode 100644 index 0000000000..cb52d7c2a8 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter/test.yaml @@ -0,0 +1,28 @@ +description: Custom counter +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_aborted_request/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_aborted_request/metrics.snap new file mode 100644 index 0000000000..919d30bb62 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_aborted_request/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_aborted_request/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_aborted_request/router.yaml new file mode 100644 index 0000000000..665fa56088 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_aborted_request/router.yaml @@ -0,0 +1,10 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_aborted_request/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_aborted_request/test.yaml new file mode 100644 index 0000000000..9e198fef7a --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_aborted_request/test.yaml @@ -0,0 +1,20 @@ +description: Custom counter aborted request, the supergraph response didn't happen, but request should increment the metric on Drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + - subgraph_response: + status: 200 + data: + hello: "world" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_custom_value/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_custom_value/metrics.snap new file mode 100644 index 0000000000..42f4e68f83 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_custom_value/metrics.snap @@ -0,0 +1,25 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: + request_header: count_header +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 10 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_custom_value/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_custom_value/router.yaml new file mode 100644 index 0000000000..14969d480d --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_custom_value/router.yaml @@ -0,0 +1,11 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: + subgraph_request_header: "count_header" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_custom_value/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_custom_value/test.yaml new file mode 100644 index 0000000000..81697a238c --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_custom_value/test.yaml @@ -0,0 +1,34 @@ +description: Custom counter that gets a value from a header +events: + - - router_request: + uri: "/hello" + method: GET + headers: + count_header: 10 + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + count_header: 10 + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + headers: + count_header: 10 + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_attributes/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_attributes/metrics.snap new file mode 100644 index 0000000000..ed04a2d04e --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_attributes/metrics.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter with attributes +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit + attributes: + subgraph.graphql.document: true + custom_attribute: + subgraph_request_header: custom_header +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: + custom_attribute: custom_value + subgraph.graphql.document: "query { hello }" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_attributes/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_attributes/router.yaml new file mode 100644 index 0000000000..80ae19158a --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_attributes/router.yaml @@ -0,0 +1,14 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit + attributes: + subgraph.graphql.document: true + "custom_attribute": + subgraph_request_header: "custom_header" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_attributes/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_attributes/test.yaml new file mode 100644 index 0000000000..bd28074e84 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_attributes/test.yaml @@ -0,0 +1,34 @@ +description: Custom counter with attributes +events: + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + headers: + custom_header: "custom_value" + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_conditions/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_conditions/metrics.snap new file mode 100644 index 0000000000..7cc4a760ff --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_conditions/metrics.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter with conditions +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit + attributes: + subgraph.graphql.document: true + custom_attribute: + subgraph_request_header: custom_header + condition: + eq: + - subgraph_request_header: custom_header + - allowed +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: + custom_attribute: allowed + subgraph.graphql.document: "query { hello }" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_conditions/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_conditions/router.yaml new file mode 100644 index 0000000000..70db0630eb --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_conditions/router.yaml @@ -0,0 +1,18 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit + attributes: + subgraph.graphql.document: true + "custom_attribute": + subgraph_request_header: "custom_header" + condition: + eq: + - subgraph_request_header: "custom_header" + - "allowed" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_conditions/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_conditions/test.yaml new file mode 100644 index 0000000000..dcc27d76d8 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_counter_with_conditions/test.yaml @@ -0,0 +1,67 @@ +description: Custom counter with conditions +events: + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: allowed + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: allowed + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + headers: + custom_header: allowed + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 + + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: not_allowed + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: not_allowed + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + headers: + custom_header: not_allowed + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_custom_value/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_custom_value/metrics.snap new file mode 100644 index 0000000000..957fec0e25 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_custom_value/metrics.snap @@ -0,0 +1,25 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram with value from custom header +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: + request_header: count_header +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 10 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_custom_value/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_custom_value/router.yaml new file mode 100644 index 0000000000..b0c4d6d756 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_custom_value/router.yaml @@ -0,0 +1,11 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: + subgraph_request_header: "count_header" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_custom_value/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_custom_value/test.yaml new file mode 100644 index 0000000000..2b2899115d --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_custom_value/test.yaml @@ -0,0 +1,34 @@ +description: Custom histogram with value from custom header +events: + - - router_request: + uri: "/hello" + method: GET + headers: + count_header: "10" + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + count_header: "10" + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + headers: + count_header: 10 + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration/metrics.snap new file mode 100644 index 0000000000..815e578506 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram duration +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram.duration: + description: histogram of requests + type: histogram + unit: unit + value: duration +--- +- name: custom.histogram.duration + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 0.1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration/router.yaml new file mode 100644 index 0000000000..57dffb0141 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration/router.yaml @@ -0,0 +1,10 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + "custom.histogram.duration": + description: "histogram of requests" + type: histogram + unit: "unit" + value: duration \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration/test.yaml new file mode 100644 index 0000000000..d4e0f5a0a7 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration/test.yaml @@ -0,0 +1,30 @@ +description: Custom histogram duration +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration_aborted_request/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration_aborted_request/metrics.snap new file mode 100644 index 0000000000..815e578506 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration_aborted_request/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram duration +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram.duration: + description: histogram of requests + type: histogram + unit: unit + value: duration +--- +- name: custom.histogram.duration + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 0.1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration_aborted_request/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration_aborted_request/router.yaml new file mode 100644 index 0000000000..57dffb0141 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration_aborted_request/router.yaml @@ -0,0 +1,10 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + "custom.histogram.duration": + description: "histogram of requests" + type: histogram + unit: "unit" + value: duration \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration_aborted_request/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration_aborted_request/test.yaml new file mode 100644 index 0000000000..2c91e72669 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration_aborted_request/test.yaml @@ -0,0 +1,18 @@ +description: Custom histogram where subgraph response doesn't happen. This should still increment the metric on Drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit/metrics.snap new file mode 100644 index 0000000000..5d4f3f4e4e --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram unit +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit/router.yaml new file mode 100644 index 0000000000..d00cabc0ff --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit/router.yaml @@ -0,0 +1,10 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit/test.yaml new file mode 100644 index 0000000000..6598440927 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit/test.yaml @@ -0,0 +1,30 @@ +description: Custom histogram unit +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit_aborted_request/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit_aborted_request/metrics.snap new file mode 100644 index 0000000000..5d4f3f4e4e --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit_aborted_request/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram unit +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit_aborted_request/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit_aborted_request/router.yaml new file mode 100644 index 0000000000..d00cabc0ff --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit_aborted_request/router.yaml @@ -0,0 +1,10 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit_aborted_request/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit_aborted_request/test.yaml new file mode 100644 index 0000000000..2c91e72669 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_unit_aborted_request/test.yaml @@ -0,0 +1,18 @@ +description: Custom histogram where subgraph response doesn't happen. This should still increment the metric on Drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_attributes/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_attributes/metrics.snap new file mode 100644 index 0000000000..a64336ff0e --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_attributes/metrics.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram with attributes +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit + attributes: + subgraph.graphql.document: true + custom_attribute: + subgraph_request_header: custom_header +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: + custom_attribute: custom_value + subgraph.graphql.document: "query { hello }" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_attributes/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_attributes/router.yaml new file mode 100644 index 0000000000..811240de7b --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_attributes/router.yaml @@ -0,0 +1,14 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit + attributes: + subgraph.graphql.document: true + "custom_attribute": + subgraph_request_header: "custom_header" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_attributes/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_attributes/test.yaml new file mode 100644 index 0000000000..abf1b8d4ba --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_attributes/test.yaml @@ -0,0 +1,34 @@ +description: Custom histogram with attributes +events: + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: "custom_value" + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + headers: + custom_header: custom_value + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_conditions/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_conditions/metrics.snap new file mode 100644 index 0000000000..5d2531612b --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_conditions/metrics.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter with conditions +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit + attributes: + subgraph.graphql.document: true + custom_attribute: + subgraph_request_header: custom_header + condition: + eq: + - subgraph_request_header: custom_header + - allowed +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: + custom_attribute: allowed + subgraph.graphql.document: "query { hello }" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_conditions/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_conditions/router.yaml new file mode 100644 index 0000000000..dc99410c1c --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_conditions/router.yaml @@ -0,0 +1,18 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit + attributes: + subgraph.graphql.document: true + "custom_attribute": + subgraph_request_header: "custom_header" + condition: + eq: + - subgraph_request_header: "custom_header" + - "allowed" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_conditions/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_conditions/test.yaml new file mode 100644 index 0000000000..5960aada90 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_with_conditions/test.yaml @@ -0,0 +1,59 @@ +description: Custom counter with conditions +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + headers: + custom_header: allowed + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 + + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + headers: + custom_header: "not_allowed" + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/attribute.on_graphql_error/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/attribute.on_graphql_error/metrics.snap new file mode 100644 index 0000000000..af8022f907 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/attribute.on_graphql_error/metrics.snap @@ -0,0 +1,27 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: on_graphql_error attribute +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit + attributes: + on.graphql.error: + on_graphql_error: true +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: + on.graphql.error: true diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/attribute.on_graphql_error/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/attribute.on_graphql_error/router.yaml new file mode 100644 index 0000000000..35ee04f547 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/attribute.on_graphql_error/router.yaml @@ -0,0 +1,13 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit + attributes: + on.graphql.error: + on_graphql_error: true diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/attribute.on_graphql_error/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/attribute.on_graphql_error/test.yaml new file mode 100644 index 0000000000..d4086d6062 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/attribute.on_graphql_error/test.yaml @@ -0,0 +1,22 @@ +description: on_graphql_error attribute +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + query: "query { hello }" + - context: + map: + "apollo::telemetry::contains_graphql_error": true + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter/metrics.snap new file mode 100644 index 0000000000..919d30bb62 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter/router.yaml new file mode 100644 index 0000000000..6628068fdf --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter/router.yaml @@ -0,0 +1,10 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter/test.yaml new file mode 100644 index 0000000000..03739fdbd9 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter/test.yaml @@ -0,0 +1,19 @@ +description: Custom counter +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + query: "query { hello }" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_aborted_request/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_aborted_request/metrics.snap new file mode 100644 index 0000000000..919d30bb62 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_aborted_request/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_aborted_request/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_aborted_request/router.yaml new file mode 100644 index 0000000000..6628068fdf --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_aborted_request/router.yaml @@ -0,0 +1,10 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_aborted_request/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_aborted_request/test.yaml new file mode 100644 index 0000000000..4ec8081e87 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_aborted_request/test.yaml @@ -0,0 +1,11 @@ +description: Custom counter aborted request, the supergraph response didn't happen, but request should increment the metric on Drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + query: "query { hello }" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_custom_value/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_custom_value/metrics.snap new file mode 100644 index 0000000000..42f4e68f83 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_custom_value/metrics.snap @@ -0,0 +1,25 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Test server request body size metrics +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom_counter: + description: count of requests + type: counter + unit: unit + value: + request_header: count_header +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 10 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_custom_value/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_custom_value/router.yaml new file mode 100644 index 0000000000..12e6c41244 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_custom_value/router.yaml @@ -0,0 +1,11 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: + request_header: "count_header" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_custom_value/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_custom_value/test.yaml new file mode 100644 index 0000000000..2635c31645 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_custom_value/test.yaml @@ -0,0 +1,23 @@ +description: Custom counter that gets a value from a header +events: + - - router_request: + uri: "/hello" + method: GET + headers: + count_header: 10 + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + count_header: 10 + query: "query { hello }" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_attributes/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_attributes/metrics.snap new file mode 100644 index 0000000000..f45bc47746 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_attributes/metrics.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter with attributes +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit + attributes: + graphql.document: true + custom_attribute: + request_header: custom_header +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: + custom_attribute: custom_value + graphql.document: "query { hello }" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_attributes/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_attributes/router.yaml new file mode 100644 index 0000000000..1387359460 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_attributes/router.yaml @@ -0,0 +1,14 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit + attributes: + graphql.document: true + "custom_attribute": + request_header: "custom_header" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_attributes/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_attributes/test.yaml new file mode 100644 index 0000000000..4f659c4482 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_attributes/test.yaml @@ -0,0 +1,23 @@ +description: Custom counter with attributes +events: + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_conditions/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_conditions/metrics.snap new file mode 100644 index 0000000000..415cc0b50e --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_conditions/metrics.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter with conditions +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + custom_counter: + description: count of requests + type: counter + unit: unit + value: unit + attributes: + graphql.document: true + custom_attribute: + request_header: custom_header + condition: + eq: + - request_header: custom_header + - allowed +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: + custom_attribute: allowed + graphql.document: "query { hello }" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_conditions/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_conditions/router.yaml new file mode 100644 index 0000000000..294189bc28 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_conditions/router.yaml @@ -0,0 +1,18 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: unit + attributes: + graphql.document: true + "custom_attribute": + request_header: "custom_header" + condition: + eq: + - request_header: "custom_header" + - "allowed" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_conditions/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_conditions/test.yaml new file mode 100644 index 0000000000..3235a8be2c --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_counter_with_conditions/test.yaml @@ -0,0 +1,45 @@ +description: Custom counter with conditions +events: + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: allowed + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: allowed + query: "query { hello }" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 + + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: not_allowed + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: not_allowed + query: "query { hello }" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_custom_value/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_custom_value/metrics.snap new file mode 100644 index 0000000000..957fec0e25 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_custom_value/metrics.snap @@ -0,0 +1,25 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram with value from custom header +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: + request_header: count_header +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 10 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_custom_value/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_custom_value/router.yaml new file mode 100644 index 0000000000..5e6187ef20 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_custom_value/router.yaml @@ -0,0 +1,11 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: + request_header: "count_header" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_custom_value/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_custom_value/test.yaml new file mode 100644 index 0000000000..6afe1ac113 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_custom_value/test.yaml @@ -0,0 +1,23 @@ +description: Custom histogram with value from custom header +events: + - - router_request: + uri: "/hello" + method: GET + headers: + count_header: "10" + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + count_header: "10" + query: "query { hello }" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration/metrics.snap new file mode 100644 index 0000000000..815e578506 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram duration +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram.duration: + description: histogram of requests + type: histogram + unit: unit + value: duration +--- +- name: custom.histogram.duration + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 0.1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration/router.yaml new file mode 100644 index 0000000000..35b853f4fa --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration/router.yaml @@ -0,0 +1,10 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom.histogram.duration": + description: "histogram of requests" + type: histogram + unit: "unit" + value: duration \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration/test.yaml new file mode 100644 index 0000000000..55136efe93 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration/test.yaml @@ -0,0 +1,21 @@ +description: Custom histogram duration +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration_aborted_request/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration_aborted_request/metrics.snap new file mode 100644 index 0000000000..815e578506 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration_aborted_request/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram duration +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram.duration: + description: histogram of requests + type: histogram + unit: unit + value: duration +--- +- name: custom.histogram.duration + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 0.1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration_aborted_request/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration_aborted_request/router.yaml new file mode 100644 index 0000000000..35b853f4fa --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration_aborted_request/router.yaml @@ -0,0 +1,10 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom.histogram.duration": + description: "histogram of requests" + type: histogram + unit: "unit" + value: duration \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration_aborted_request/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration_aborted_request/test.yaml new file mode 100644 index 0000000000..55136efe93 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_duration_aborted_request/test.yaml @@ -0,0 +1,21 @@ +description: Custom histogram duration +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit/metrics.snap new file mode 100644 index 0000000000..5d4f3f4e4e --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram unit +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit/router.yaml new file mode 100644 index 0000000000..1a3ef169a0 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit/router.yaml @@ -0,0 +1,10 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit/test.yaml new file mode 100644 index 0000000000..b110fd25bb --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit/test.yaml @@ -0,0 +1,13 @@ +description: Custom histogram where supergraph response doesn't happen. This should still increment the metric on Drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit_aborted_request/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit_aborted_request/metrics.snap new file mode 100644 index 0000000000..5d4f3f4e4e --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit_aborted_request/metrics.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram unit +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + router: + http.server.active_requests: false + http.server.request.duration: false + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: {} diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit_aborted_request/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit_aborted_request/router.yaml new file mode 100644 index 0000000000..1a3ef169a0 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit_aborted_request/router.yaml @@ -0,0 +1,10 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit_aborted_request/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit_aborted_request/test.yaml new file mode 100644 index 0000000000..b110fd25bb --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_unit_aborted_request/test.yaml @@ -0,0 +1,13 @@ +description: Custom histogram where supergraph response doesn't happen. This should still increment the metric on Drop. +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_attributes/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_attributes/metrics.snap new file mode 100644 index 0000000000..14b31dba79 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_attributes/metrics.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom histogram with attributes +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit + attributes: + graphql.document: true + custom_attribute: + request_header: custom_header +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: + custom_attribute: custom_value + graphql.document: "query { hello }" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_attributes/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_attributes/router.yaml new file mode 100644 index 0000000000..87535b9947 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_attributes/router.yaml @@ -0,0 +1,14 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit + attributes: + graphql.document: true + "custom_attribute": + request_header: "custom_header" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_attributes/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_attributes/test.yaml new file mode 100644 index 0000000000..05ec919f35 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_attributes/test.yaml @@ -0,0 +1,23 @@ +description: Custom histogram with attributes +events: + - - router_request: + uri: "/hello" + method: GET + headers: + custom_header: "custom_value" + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_conditions/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_conditions/metrics.snap new file mode 100644 index 0000000000..6afdc30d2f --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_conditions/metrics.snap @@ -0,0 +1,33 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Custom counter with conditions +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + custom.histogram: + description: histogram of requests + type: histogram + unit: unit + value: unit + attributes: + graphql.document: true + custom_attribute: + request_header: custom_header + condition: + eq: + - request_header: custom_header + - allowed +--- +- name: custom.histogram + description: histogram of requests + unit: unit + data: + datapoints: + - sum: 1 + attributes: + custom_attribute: allowed + graphql.document: "query { hello }" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_conditions/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_conditions/router.yaml new file mode 100644 index 0000000000..f87825b7b8 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_conditions/router.yaml @@ -0,0 +1,18 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom.histogram": + description: "histogram of requests" + type: histogram + unit: "unit" + value: unit + attributes: + graphql.document: true + "custom_attribute": + request_header: "custom_header" + condition: + eq: + - request_header: "custom_header" + - "allowed" diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_conditions/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_conditions/test.yaml new file mode 100644 index 0000000000..78eba9955a --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/custom_histogram_with_conditions/test.yaml @@ -0,0 +1,41 @@ +description: Custom counter with conditions +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: allowed + query: "query { hello }" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 + + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: not_allowed + query: "query { hello }" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/event.attributes.on_graphql_error/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/event.attributes.on_graphql_error/metrics.snap new file mode 100644 index 0000000000..1fdbf7a1f7 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/event.attributes.on_graphql_error/metrics.snap @@ -0,0 +1,27 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: on_graphql_error attribute +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + custom_counter: + description: count of requests + type: counter + unit: unit + value: event_unit + attributes: + on.graphql.error: + on_graphql_error: true +--- +- name: custom_counter + description: count of requests + unit: unit + data: + datapoints: + - value: 1 + attributes: + on.graphql.error: true diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/event.attributes.on_graphql_error/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/event.attributes.on_graphql_error/router.yaml new file mode 100644 index 0000000000..4f4ef57519 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/event.attributes.on_graphql_error/router.yaml @@ -0,0 +1,13 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + supergraph: + "custom_counter": + description: "count of requests" + type: counter + unit: "unit" + value: event_unit + attributes: + on.graphql.error: + on_graphql_error: true diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/event.attributes.on_graphql_error/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/event.attributes.on_graphql_error/test.yaml new file mode 100644 index 0000000000..f3ca06a5a7 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph/event.attributes.on_graphql_error/test.yaml @@ -0,0 +1,25 @@ +description: on_graphql_error attribute +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + query: "query { hello }" + - context: + map: + "apollo::telemetry::contains_graphql_error": true + - graphql_response: + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph_instruments.router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph_instruments.router.yaml new file mode 100644 index 0000000000..4fdccc46b9 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/supergraph_instruments.router.yaml @@ -0,0 +1,48 @@ +telemetry: + instrumentation: + instruments: + + supergraph: + acme.request.on_error: + value: unit + type: counter + unit: error + description: my description + condition: + not: + eq: + - 200 + - response_status: code + acme.request.on_graphql_error: + value: event_unit + type: counter + unit: error + description: my description + condition: + eq: + - NOPE + - response_errors: "$.[0].extensions.code" + attributes: + response_errors: + response_errors: "$.*" + acme.request.on_graphql_data: + value: + response_data: "$.price" + type: counter + unit: "$" + description: my description + attributes: + response.data: + response_data: "$.*" + acme.query: + value: unit + type: counter + description: nb of queries + condition: + eq: + - query + - operation_kind: string + unit: query + attributes: + query: + query: string diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs new file mode 100644 index 0000000000..36f1b568a3 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs @@ -0,0 +1,207 @@ +use opentelemetry_api::KeyValue; +use schemars::JsonSchema; +use serde::Deserialize; +use tower::BoxError; + +use crate::plugins::demand_control::cost_calculator::schema_aware_response::TypedValue; +use crate::plugins::telemetry::config_new::graphql::selectors::FieldName; +use crate::plugins::telemetry::config_new::graphql::selectors::FieldType; +use crate::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector; +use crate::plugins::telemetry::config_new::graphql::selectors::ListLength; +use crate::plugins::telemetry::config_new::graphql::selectors::TypeName; +use crate::plugins::telemetry::config_new::selectors::OperationName; +use crate::plugins::telemetry::config_new::DefaultAttributeRequirementLevel; +use crate::plugins::telemetry::config_new::DefaultForLevel; +use crate::plugins::telemetry::config_new::Selector; +use crate::plugins::telemetry::config_new::Selectors; +use crate::plugins::telemetry::otlp::TelemetryDataKind; +use crate::services::supergraph; +use crate::Context; + +#[derive(Deserialize, JsonSchema, Clone, Default, Debug, PartialEq)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct GraphQLAttributes { + /// The GraphQL field name + #[serde(rename = "graphql.field.name")] + pub(crate) field_name: Option, + /// The GraphQL field type + #[serde(rename = "graphql.field.type")] + pub(crate) field_type: Option, + /// If the field is a list, the length of the list + #[serde(rename = "graphql.list.length")] + pub(crate) list_length: Option, + /// The GraphQL operation name + #[serde(rename = "graphql.operation.name")] + pub(crate) operation_name: Option, + /// The GraphQL type name + #[serde(rename = "graphql.type.name")] + pub(crate) type_name: Option, +} + +impl DefaultForLevel for GraphQLAttributes { + fn defaults_for_level( + &mut self, + requirement_level: DefaultAttributeRequirementLevel, + kind: TelemetryDataKind, + ) { + if let TelemetryDataKind::Metrics = kind { + if let DefaultAttributeRequirementLevel::Required = requirement_level { + self.field_name.get_or_insert(true); + self.field_type.get_or_insert(true); + self.type_name.get_or_insert(true); + } + } + } +} + +impl Selectors for GraphQLAttributes { + type Request = supergraph::Request; + type Response = supergraph::Response; + type EventResponse = crate::graphql::Response; + + fn on_request(&self, _request: &Self::Request) -> Vec { + Vec::default() + } + + fn on_response(&self, _response: &Self::Response) -> Vec { + Vec::default() + } + + fn on_error(&self, _error: &BoxError) -> Vec { + Vec::default() + } + + fn on_response_field(&self, typed_value: &TypedValue, ctx: &Context) -> Vec { + let mut attrs = Vec::with_capacity(4); + if let Some(true) = self.field_name { + if let Some(name) = (GraphQLSelector::FieldName { + field_name: FieldName::String, + }) + .on_response_field(typed_value, ctx) + { + attrs.push(KeyValue::new("graphql.field.name", name)); + } + } + if let Some(true) = self.field_type { + if let Some(ty) = (GraphQLSelector::FieldType { + field_type: FieldType::Name, + }) + .on_response_field(typed_value, ctx) + { + attrs.push(KeyValue::new("graphql.field.type", ty)); + } + } + if let Some(true) = self.type_name { + if let Some(ty) = (GraphQLSelector::TypeName { + type_name: TypeName::String, + }) + .on_response_field(typed_value, ctx) + { + attrs.push(KeyValue::new("graphql.type.name", ty)); + } + } + if let Some(true) = self.list_length { + if let Some(length) = (GraphQLSelector::ListLength { + list_length: ListLength::Value, + }) + .on_response_field(typed_value, ctx) + { + attrs.push(KeyValue::new("graphql.list.length", length)); + } + } + if let Some(true) = self.operation_name { + if let Some(length) = (GraphQLSelector::OperationName { + operation_name: OperationName::String, + default: None, + }) + .on_response_field(typed_value, ctx) + { + attrs.push(KeyValue::new("graphql.operation.name", length)); + } + } + attrs + } +} + +#[cfg(test)] +mod test { + use crate::context::OPERATION_NAME; + use crate::plugins::demand_control::cost_calculator::schema_aware_response::TypedValue; + use crate::plugins::telemetry::config_new::test::field; + use crate::plugins::telemetry::config_new::test::ty; + use crate::plugins::telemetry::config_new::DefaultForLevel; + use crate::plugins::telemetry::config_new::Selectors; + use crate::Context; + + #[test] + fn test_default_for_level() { + let mut attributes = super::GraphQLAttributes::default(); + attributes.defaults_for_level( + super::DefaultAttributeRequirementLevel::Required, + super::TelemetryDataKind::Metrics, + ); + assert_eq!(attributes.field_name, Some(true)); + assert_eq!(attributes.field_type, Some(true)); + assert_eq!(attributes.type_name, Some(true)); + assert_eq!(attributes.list_length, None); + assert_eq!(attributes.operation_name, None); + } + + #[test] + fn test_on_response_field_non_list() { + let attributes = super::GraphQLAttributes { + field_name: Some(true), + field_type: Some(true), + list_length: Some(true), + operation_name: Some(true), + type_name: Some(true), + }; + let typed_value = TypedValue::Bool(ty(), field(), &true); + let ctx = Context::default(); + let _ = ctx.insert(OPERATION_NAME, "operation_name".to_string()); + let result = attributes.on_response_field(&typed_value, &ctx); + assert_eq!(result.len(), 4); + assert_eq!(result[0].key.as_str(), "graphql.field.name"); + assert_eq!(result[0].value.as_str(), "field_name"); + assert_eq!(result[1].key.as_str(), "graphql.field.type"); + assert_eq!(result[1].value.as_str(), "field_type"); + assert_eq!(result[2].key.as_str(), "graphql.type.name"); + assert_eq!(result[2].value.as_str(), "type_name"); + assert_eq!(result[3].key.as_str(), "graphql.operation.name"); + assert_eq!(result[3].value.as_str(), "operation_name"); + } + + #[test] + fn test_on_response_field_list() { + let attributes = super::GraphQLAttributes { + field_name: Some(true), + field_type: Some(true), + list_length: Some(true), + operation_name: Some(true), + type_name: Some(true), + }; + let typed_value = TypedValue::List( + ty(), + field(), + vec![ + TypedValue::Bool(ty(), field(), &true), + TypedValue::Bool(ty(), field(), &true), + TypedValue::Bool(ty(), field(), &true), + ], + ); + let ctx = Context::default(); + let _ = ctx.insert(OPERATION_NAME, "operation_name".to_string()); + let result = attributes.on_response_field(&typed_value, &ctx); + assert_eq!(result.len(), 5); + assert_eq!(result[0].key.as_str(), "graphql.field.name"); + assert_eq!(result[0].value.as_str(), "field_name"); + assert_eq!(result[1].key.as_str(), "graphql.field.type"); + assert_eq!(result[1].value.as_str(), "field_type"); + assert_eq!(result[2].key.as_str(), "graphql.type.name"); + assert_eq!(result[2].value.as_str(), "type_name"); + assert_eq!(result[3].key.as_str(), "graphql.list.length"); + assert_eq!(result[3].value.as_str(), "3"); + assert_eq!(result[4].key.as_str(), "graphql.operation.name"); + assert_eq!(result[4].value.as_str(), "operation_name"); + } +} diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/fixtures/field_length_disabled.router.yaml b/apollo-router/src/plugins/telemetry/config_new/graphql/fixtures/field_length_disabled.router.yaml new file mode 100644 index 0000000000..fa39700013 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/fixtures/field_length_disabled.router.yaml @@ -0,0 +1,5 @@ +telemetry: + instrumentation: + instruments: + graphql: + list.length: false diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/fixtures/field_length_enabled.router.yaml b/apollo-router/src/plugins/telemetry/config_new/graphql/fixtures/field_length_enabled.router.yaml new file mode 100644 index 0000000000..83b2bb8d8f --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/fixtures/field_length_enabled.router.yaml @@ -0,0 +1,5 @@ +telemetry: + instrumentation: + instruments: + graphql: + list.length: true diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/fixtures/filtered_field_length.router.yaml b/apollo-router/src/plugins/telemetry/config_new/graphql/fixtures/filtered_field_length.router.yaml new file mode 100644 index 0000000000..6c02c9c450 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/fixtures/filtered_field_length.router.yaml @@ -0,0 +1,15 @@ +telemetry: + instrumentation: + instruments: + graphql: + "ships.list.length": + description: test + type: histogram + unit: count + value: + field_custom: + list_length: value + condition: + eq: + - field_name: string + - "ships" diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs b/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs new file mode 100644 index 0000000000..42eebce745 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs @@ -0,0 +1,437 @@ +use std::sync::Arc; + +use opentelemetry::metrics::MeterProvider; +use parking_lot::Mutex; +use schemars::JsonSchema; +use serde::Deserialize; +use tower::BoxError; + +use super::instruments::CustomCounter; +use super::instruments::CustomCounterInner; +use super::instruments::CustomInstruments; +use super::instruments::Increment; +use super::instruments::InstrumentsConfig; +use super::instruments::METER_NAME; +use crate::metrics; +use crate::plugins::demand_control::cost_calculator::schema_aware_response; +use crate::plugins::demand_control::cost_calculator::schema_aware_response::TypedValue; +use crate::plugins::demand_control::cost_calculator::schema_aware_response::Visitor; +use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel; +use crate::plugins::telemetry::config_new::conditions::Condition; +use crate::plugins::telemetry::config_new::extendable::Extendable; +use crate::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes; +use crate::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector; +use crate::plugins::telemetry::config_new::graphql::selectors::ListLength; +use crate::plugins::telemetry::config_new::instruments::CustomHistogram; +use crate::plugins::telemetry::config_new::instruments::CustomHistogramInner; +use crate::plugins::telemetry::config_new::instruments::DefaultedStandardInstrument; +use crate::plugins::telemetry::config_new::instruments::Instrumented; +use crate::plugins::telemetry::config_new::DefaultForLevel; +use crate::plugins::telemetry::otlp::TelemetryDataKind; +use crate::services::supergraph; +use crate::Context; + +pub(crate) mod attributes; +pub(crate) mod selectors; + +static FIELD_LENGTH: &str = "graphql.field.list.length"; +static FIELD_EXECUTION: &str = "graphql.field.execution"; + +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct GraphQLInstrumentsConfig { + /// A histogram of the length of a selected field in the GraphQL response + #[serde(rename = "list.length")] + pub(crate) list_length: + DefaultedStandardInstrument>, + + /// A counter of the number of times a field is used. + #[serde(rename = "field.execution")] + pub(crate) field_execution: + DefaultedStandardInstrument>, +} + +impl DefaultForLevel for GraphQLInstrumentsConfig { + fn defaults_for_level( + &mut self, + requirement_level: DefaultAttributeRequirementLevel, + kind: TelemetryDataKind, + ) { + if self.list_length.is_enabled() { + self.list_length.defaults_for_level(requirement_level, kind); + } + if self.field_execution.is_enabled() { + self.field_execution + .defaults_for_level(requirement_level, kind); + } + } +} + +pub(crate) type GraphQLCustomInstruments = CustomInstruments< + supergraph::Request, + supergraph::Response, + GraphQLAttributes, + GraphQLSelector, +>; + +pub(crate) struct GraphQLInstruments { + pub(crate) list_length: Option< + CustomHistogram< + supergraph::Request, + supergraph::Response, + GraphQLAttributes, + GraphQLSelector, + >, + >, + pub(crate) field_execution: Option< + CustomCounter< + supergraph::Request, + supergraph::Response, + GraphQLAttributes, + GraphQLSelector, + >, + >, + pub(crate) custom: GraphQLCustomInstruments, +} + +impl From<&InstrumentsConfig> for GraphQLInstruments { + fn from(value: &InstrumentsConfig) -> Self { + let meter = metrics::meter_provider().meter(METER_NAME); + GraphQLInstruments { + list_length: value.graphql.attributes.list_length.is_enabled().then(|| { + let mut nb_attributes = 0; + let selectors = match &value.graphql.attributes.list_length { + DefaultedStandardInstrument::Bool(_) | DefaultedStandardInstrument::Unset => { + None + } + DefaultedStandardInstrument::Extendable { attributes } => { + nb_attributes = attributes.custom.len(); + Some(attributes.clone()) + } + }; + CustomHistogram { + inner: Mutex::new(CustomHistogramInner { + increment: Increment::FieldCustom(None), + condition: Condition::True, + histogram: Some(meter.f64_histogram(FIELD_LENGTH).init()), + attributes: Vec::with_capacity(nb_attributes), + selector: Some(Arc::new(GraphQLSelector::ListLength { + list_length: ListLength::Value, + })), + selectors, + updated: false, + }), + } + }), + field_execution: value + .graphql + .attributes + .field_execution + .is_enabled() + .then(|| { + let mut nb_attributes = 0; + let selectors = match &value.graphql.attributes.field_execution { + DefaultedStandardInstrument::Bool(_) + | DefaultedStandardInstrument::Unset => None, + DefaultedStandardInstrument::Extendable { attributes } => { + nb_attributes = attributes.custom.len(); + Some(attributes.clone()) + } + }; + CustomCounter { + inner: Mutex::new(CustomCounterInner { + increment: Increment::FieldUnit, + condition: Condition::True, + counter: Some(meter.f64_counter(FIELD_EXECUTION).init()), + attributes: Vec::with_capacity(nb_attributes), + selector: None, + selectors, + incremented: false, + }), + } + }), + custom: CustomInstruments::new(&value.graphql.custom), + } + } +} + +impl Instrumented for GraphQLInstruments { + type Request = supergraph::Request; + type Response = supergraph::Response; + type EventResponse = crate::graphql::Response; + + fn on_request(&self, request: &Self::Request) { + if let Some(field_length) = &self.list_length { + field_length.on_request(request); + } + if let Some(field_execution) = &self.field_execution { + field_execution.on_request(request); + } + self.custom.on_request(request); + } + + fn on_response(&self, response: &Self::Response) { + if let Some(field_length) = &self.list_length { + field_length.on_response(response); + } + if let Some(field_execution) = &self.field_execution { + field_execution.on_response(response); + } + self.custom.on_response(response); + } + + fn on_error(&self, error: &BoxError, ctx: &crate::Context) { + if let Some(field_length) = &self.list_length { + field_length.on_error(error, ctx); + } + if let Some(field_execution) = &self.field_execution { + field_execution.on_error(error, ctx); + } + self.custom.on_error(error, ctx); + } + + fn on_response_event(&self, response: &Self::EventResponse, ctx: &Context) { + if !self.custom.is_empty() || self.list_length.is_some() || self.field_execution.is_some() { + if let Some(executable_document) = ctx.unsupported_executable_document() { + if let Ok(schema) = + schema_aware_response::SchemaAwareResponse::new(&executable_document, response) + { + GraphQLInstrumentsVisitor { + ctx, + instruments: self, + } + .visit(&schema.value) + } + } + } + } + + fn on_response_field(&self, typed_value: &TypedValue, ctx: &Context) { + if let Some(field_length) = &self.list_length { + field_length.on_response_field(typed_value, ctx); + } + if let Some(field_execution) = &self.field_execution { + field_execution.on_response_field(typed_value, ctx); + } + self.custom.on_response_field(typed_value, ctx); + } +} + +struct GraphQLInstrumentsVisitor<'a> { + ctx: &'a Context, + instruments: &'a GraphQLInstruments, +} + +impl<'a> Visitor for GraphQLInstrumentsVisitor<'a> { + fn visit_field(&self, value: &TypedValue) { + self.instruments.on_response_field(value, self.ctx); + } +} + +#[cfg(test)] +pub(crate) mod test { + + use super::*; + use crate::metrics::FutureMetricsExt; + use crate::plugins::telemetry::Telemetry; + use crate::plugins::test::PluginTestHarness; + use crate::Configuration; + + #[test_log::test(tokio::test)] + async fn basic_metric_publishing() { + async { + let schema_str = include_str!( + "../../../demand_control/cost_calculator/fixtures/federated_ships_schema.graphql" + ); + let query_str = include_str!("../../../demand_control/cost_calculator/fixtures/federated_ships_named_query.graphql"); + + + let request = supergraph::Request::fake_builder() + .query(query_str) + .context(context(schema_str, query_str)) + .build() + .unwrap(); + + let harness = PluginTestHarness::::builder() + .config(include_str!("fixtures/field_length_enabled.router.yaml")) + .schema(schema_str) + .build() + .await; + + harness + .call_supergraph(request, |req| { + let response: serde_json::Value = serde_json::from_str(include_str!( + "../../../demand_control/cost_calculator/fixtures/federated_ships_named_response.json" + )) + .unwrap(); + supergraph::Response::builder() + .data(response["data"].clone()) + .context(req.context) + .build() + .unwrap() + }) + .await + .unwrap(); + + assert_histogram_sum!( + "graphql.field.list.length", + 2.0, + "graphql.field.name" = "users", + "graphql.field.type" = "User", + "graphql.type.name" = "Query" + ); + } + .with_metrics() + .await; + } + + #[test_log::test(tokio::test)] + async fn multiple_fields_metric_publishing() { + async { + let schema_str = include_str!( + "../../../demand_control/cost_calculator/fixtures/federated_ships_schema.graphql" + ); + let query_str = include_str!("../../../demand_control/cost_calculator/fixtures/federated_ships_fragment_query.graphql"); + + + let request = supergraph::Request::fake_builder() + .query(query_str) + .context(context(schema_str, query_str)) + .build() + .unwrap(); + + let harness = PluginTestHarness::::builder() + .config(include_str!("fixtures/field_length_enabled.router.yaml")) + .schema(schema_str) + .build() + .await; + + harness + .call_supergraph(request, |req| { + let response: serde_json::Value = serde_json::from_str(include_str!( + "../../../demand_control/cost_calculator/fixtures/federated_ships_fragment_response.json" + )) + .unwrap(); + supergraph::Response::builder() + .data(response["data"].clone()) + .context(req.context) + .build() + .unwrap() + }) + .await + .unwrap(); + + assert_histogram_sum!( + "graphql.field.list.length", + 2.0, + "graphql.field.name" = "ships", + "graphql.field.type" = "Ship", + "graphql.type.name" = "Query" + ); + assert_histogram_sum!( + "graphql.field.list.length", + 2.0, + "graphql.field.name" = "users", + "graphql.field.type" = "User", + "graphql.type.name" = "Query" + ); + } + .with_metrics() + .await; + } + + #[test_log::test(tokio::test)] + async fn disabled_metric_publishing() { + async { + let schema_str = include_str!( + "../../../demand_control/cost_calculator/fixtures/federated_ships_schema.graphql" + ); + let query_str = include_str!("../../../demand_control/cost_calculator/fixtures/federated_ships_named_query.graphql"); + + + let request = supergraph::Request::fake_builder() + .query(query_str) + .context(context(schema_str, query_str)) + .build() + .unwrap(); + + let harness = PluginTestHarness::::builder() + .config(include_str!("fixtures/field_length_disabled.router.yaml")) + .schema(schema_str) + .build() + .await; + + harness + .call_supergraph(request, |req| { + let response: serde_json::Value = serde_json::from_str(include_str!( + "../../../demand_control/cost_calculator/fixtures/federated_ships_named_response.json" + )) + .unwrap(); + supergraph::Response::builder() + .data(response["data"].clone()) + .context(req.context) + .build() + .unwrap() + }) + .await + .unwrap(); + + assert_histogram_not_exists!("graphql.field.list.length", f64); + } + .with_metrics() + .await; + } + + #[test_log::test(tokio::test)] + async fn filtered_metric_publishing() { + async { + let schema_str = include_str!( + "../../../demand_control/cost_calculator/fixtures/federated_ships_schema.graphql" + ); + let query_str = include_str!("../../../demand_control/cost_calculator/fixtures/federated_ships_fragment_query.graphql"); + + + let request = supergraph::Request::fake_builder() + .query(query_str) + .context(context(schema_str, query_str)) + .build() + .unwrap(); + + let harness = PluginTestHarness::::builder() + .config(include_str!("fixtures/filtered_field_length.router.yaml")) + .schema(schema_str) + .build() + .await; + + harness + .call_supergraph(request, |req| { + let response: serde_json::Value = serde_json::from_str(include_str!( + "../../../demand_control/cost_calculator/fixtures/federated_ships_fragment_response.json" + )) + .unwrap(); + supergraph::Response::builder() + .data(response["data"].clone()) + .context(req.context) + .build() + .unwrap() + }) + .await + .unwrap(); + + assert_histogram_sum!("ships.list.length", 2.0); + } + .with_metrics() + .await; + } + + fn context(schema_str: &str, query_str: &str) -> Context { + let schema = crate::spec::Schema::parse_test(schema_str, &Default::default()).unwrap(); + let query = + crate::spec::Query::parse_document(query_str, None, &schema, &Configuration::default()) + .unwrap(); + let context = Context::new(); + context.extensions().lock().insert(query); + + context + } +} diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/selectors.rs b/apollo-router/src/plugins/telemetry/config_new/graphql/selectors.rs new file mode 100644 index 0000000000..46cf6f5c44 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/selectors.rs @@ -0,0 +1,329 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use sha2::Digest; +use tower::BoxError; + +use crate::context::OPERATION_NAME; +use crate::plugins::demand_control::cost_calculator::schema_aware_response::TypedValue; +use crate::plugins::telemetry::config::AttributeValue; +use crate::plugins::telemetry::config_new::selectors::OperationName; +use crate::plugins::telemetry::config_new::Selector; +use crate::Context; + +#[derive(Deserialize, JsonSchema, Clone, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum ListLength { + /// The length of the list + Value, +} + +#[derive(Deserialize, JsonSchema, Clone, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum FieldName { + /// The GraphQL field name + String, +} + +#[derive(Deserialize, JsonSchema, Clone, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum FieldType { + /// The GraphQL field name + Name, + /// The GraphQL field type + /// - `bool` + /// - `number` + /// - `scalar` + /// - `object` + /// - `list` + Type, +} + +#[derive(Deserialize, JsonSchema, Clone, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum TypeName { + /// The GraphQL type name + String, +} + +#[derive(Deserialize, JsonSchema, Clone, Debug)] +#[serde(deny_unknown_fields, untagged)] +pub(crate) enum GraphQLSelector { + /// If the field is a list, the length of the list + ListLength { + #[allow(dead_code)] + list_length: ListLength, + }, + /// The GraphQL field name + FieldName { + #[allow(dead_code)] + field_name: FieldName, + }, + /// The GraphQL field type + FieldType { + #[allow(dead_code)] + field_type: FieldType, + }, + /// The GraphQL type name + TypeName { + #[allow(dead_code)] + type_name: TypeName, + }, + OperationName { + /// The operation name from the query. + operation_name: OperationName, + /// Optional default value. + default: Option, + }, + StaticField { + /// A static value + r#static: AttributeValue, + }, +} + +impl Selector for GraphQLSelector { + type Request = crate::services::supergraph::Request; + type Response = crate::services::supergraph::Response; + type EventResponse = crate::graphql::Response; + + fn on_request(&self, _request: &Self::Request) -> Option { + None + } + + fn on_response(&self, _response: &Self::Response) -> Option { + None + } + + fn on_error(&self, _error: &BoxError) -> Option { + None + } + + fn on_response_field( + &self, + typed_value: &TypedValue, + ctx: &Context, + ) -> Option { + match self { + GraphQLSelector::ListLength { .. } => match typed_value { + TypedValue::List(_, _, array) => Some((array.len() as i64).into()), + _ => None, + }, + GraphQLSelector::FieldName { .. } => match typed_value { + TypedValue::Null => None, + TypedValue::Bool(_, f, _) + | TypedValue::Number(_, f, _) + | TypedValue::String(_, f, _) + | TypedValue::List(_, f, _) + | TypedValue::Object(_, f, _) => Some(f.name.to_string().into()), + TypedValue::Root(_) => None, + }, + GraphQLSelector::FieldType { + field_type: FieldType::Name, + } => match typed_value { + TypedValue::Null => None, + TypedValue::Bool(_, f, _) + | TypedValue::Number(_, f, _) + | TypedValue::String(_, f, _) + | TypedValue::List(_, f, _) + | TypedValue::Object(_, f, _) => { + Some(f.definition.ty.inner_named_type().to_string().into()) + } + TypedValue::Root(_) => None, + }, + GraphQLSelector::FieldType { + field_type: FieldType::Type, + } => match typed_value { + TypedValue::Null => None, + TypedValue::Bool(_, _, _) => Some("scalar".into()), + TypedValue::Number(_, _, _) => Some("scalar".into()), + TypedValue::String(_, _, _) => Some("scalar".into()), + TypedValue::Object(_, _, _) => Some("object".into()), + TypedValue::List(_, _, _) => Some("list".into()), + TypedValue::Root(_) => Some("object".into()), + }, + GraphQLSelector::TypeName { .. } => match typed_value { + TypedValue::Null => None, + TypedValue::Bool(ty, _, _) + | TypedValue::Number(ty, _, _) + | TypedValue::String(ty, _, _) + | TypedValue::List(ty, _, _) + | TypedValue::Object(ty, _, _) => Some(ty.to_string().into()), + TypedValue::Root(_) => None, + }, + GraphQLSelector::StaticField { r#static } => Some(r#static.clone().into()), + GraphQLSelector::OperationName { + operation_name, + default, + } => { + let op_name = ctx.get(OPERATION_NAME).ok().flatten(); + match operation_name { + OperationName::String => op_name.or_else(|| default.clone()), + OperationName::Hash => op_name.or_else(|| default.clone()).map(|op_name| { + let mut hasher = sha2::Sha256::new(); + hasher.update(op_name.as_bytes()); + let result = hasher.finalize(); + hex::encode(result) + }), + } + .map(opentelemetry::Value::from) + } + } + } +} + +#[cfg(test)] +mod tests { + use opentelemetry::Value; + + use super::*; + use crate::plugins::telemetry::config_new::test::field; + use crate::plugins::telemetry::config_new::test::ty; + + #[test] + fn array_length() { + let selector = GraphQLSelector::ListLength { + list_length: ListLength::Value, + }; + let typed_value = TypedValue::List( + ty(), + field(), + vec![ + TypedValue::Bool(ty(), field(), &true), + TypedValue::Bool(ty(), field(), &true), + TypedValue::Bool(ty(), field(), &true), + ], + ); + let result = selector.on_response_field(&typed_value, &Context::default()); + assert_eq!(result, Some(Value::I64(3))); + } + + #[test] + fn field_name() { + let selector = GraphQLSelector::FieldName { + field_name: FieldName::String, + }; + let typed_value = TypedValue::Bool(ty(), field(), &true); + let result = selector.on_response_field(&typed_value, &Context::default()); + assert_eq!(result, Some(Value::String("field_name".into()))); + } + + #[test] + fn field_type() { + let selector = GraphQLSelector::FieldType { + field_type: FieldType::Name, + }; + let typed_value = TypedValue::Bool(ty(), field(), &true); + let result = selector.on_response_field(&typed_value, &Context::default()); + assert_eq!(result, Some(Value::String("field_type".into()))); + } + + #[test] + fn field_type_scalar_type() { + assert_scalar(&TypedValue::String(ty(), field(), "value")); + assert_scalar(&TypedValue::Number( + ty(), + field(), + &serde_json::Number::from(1), + )); + } + + fn assert_scalar(typed_value: &TypedValue) { + let result = GraphQLSelector::FieldType { + field_type: FieldType::Type, + } + .on_response_field(typed_value, &Context::default()); + assert_eq!(result, Some(Value::String("scalar".into()))); + } + + #[test] + fn field_type_object_type() { + let selector = GraphQLSelector::FieldType { + field_type: FieldType::Type, + }; + let typed_value = TypedValue::Object(ty(), field(), [].into()); + let result = selector.on_response_field(&typed_value, &Context::default()); + assert_eq!(result, Some(Value::String("object".into()))); + } + + #[test] + fn field_type_root_object_type() { + let selector = GraphQLSelector::FieldType { + field_type: FieldType::Type, + }; + let typed_value = TypedValue::Root(Default::default()); + let result = selector.on_response_field(&typed_value, &Context::default()); + assert_eq!(result, Some(Value::String("object".into()))); + } + + #[test] + fn field_type_list_type() { + let selector = GraphQLSelector::FieldType { + field_type: FieldType::Type, + }; + let typed_value = + TypedValue::List(ty(), field(), vec![TypedValue::Bool(ty(), field(), &true)]); + let result = selector.on_response_field(&typed_value, &Context::default()); + assert_eq!(result, Some(Value::String("list".into()))); + } + + #[test] + fn type_name() { + let selector = GraphQLSelector::TypeName { + type_name: TypeName::String, + }; + let typed_value = TypedValue::Bool(ty(), field(), &true); + let result = selector.on_response_field(&typed_value, &Context::default()); + assert_eq!(result, Some(Value::String("type_name".into()))); + } + + #[test] + fn static_field() { + let selector = GraphQLSelector::StaticField { + r#static: "static_value".into(), + }; + let typed_value = TypedValue::Bool(ty(), field(), &true); + let result = selector.on_response_field(&typed_value, &Context::default()); + assert_eq!(result, Some(Value::String("static_value".into()))); + } + + #[test] + fn operation_name() { + let selector = GraphQLSelector::OperationName { + operation_name: OperationName::String, + default: None, + }; + let typed_value = TypedValue::Bool(ty(), field(), &true); + let ctx = Context::default(); + let _ = ctx.insert(OPERATION_NAME, "some-operation".to_string()); + let result = selector.on_response_field(&typed_value, &ctx); + assert_eq!(result, Some(Value::String("some-operation".into()))); + } + + #[test] + fn operation_name_hash() { + let selector = GraphQLSelector::OperationName { + operation_name: OperationName::Hash, + default: None, + }; + let typed_value = TypedValue::Bool(ty(), field(), &true); + let ctx = Context::default(); + let _ = ctx.insert(OPERATION_NAME, "some-operation".to_string()); + let result = selector.on_response_field(&typed_value, &ctx); + assert_eq!( + result, + Some(Value::String( + "1d507f770a74cffd6cb014b190ea31160d442ff41d9bde088b634847aeafaafd".into() + )) + ); + } + + #[test] + fn operation_name_defaulted() { + let selector = GraphQLSelector::OperationName { + operation_name: OperationName::String, + default: Some("no-operation".to_string()), + }; + let typed_value = TypedValue::Bool(ty(), field(), &true); + let result = selector.on_response_field(&typed_value, &Context::default()); + assert_eq!(result, Some(Value::String("no-operation".into()))); + } +} diff --git a/apollo-router/src/plugins/telemetry/config_new/instruments.rs b/apollo-router/src/plugins/telemetry/config_new/instruments.rs index 36c1c7d6f2..31a9a85132 100644 --- a/apollo-router/src/plugins/telemetry/config_new/instruments.rs +++ b/apollo-router/src/plugins/telemetry/config_new/instruments.rs @@ -22,6 +22,7 @@ use super::attributes::HttpServerAttributes; use super::DefaultForLevel; use super::Selector; use crate::metrics; +use crate::plugins::demand_control::cost_calculator::schema_aware_response::TypedValue; use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel; use crate::plugins::telemetry::config_new::attributes::RouterAttributes; use crate::plugins::telemetry::config_new::attributes::SubgraphAttributes; @@ -30,6 +31,9 @@ use crate::plugins::telemetry::config_new::conditions::Condition; use crate::plugins::telemetry::config_new::cost::CostInstruments; use crate::plugins::telemetry::config_new::cost::CostInstrumentsConfig; use crate::plugins::telemetry::config_new::extendable::Extendable; +use crate::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes; +use crate::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector; +use crate::plugins::telemetry::config_new::graphql::GraphQLInstrumentsConfig; use crate::plugins::telemetry::config_new::selectors::RouterSelector; use crate::plugins::telemetry::config_new::selectors::SubgraphSelector; use crate::plugins::telemetry::config_new::selectors::SupergraphSelector; @@ -59,6 +63,9 @@ pub(crate) struct InstrumentsConfig { /// Subgraph service instruments. For more information see documentation on Router lifecycle. pub(crate) subgraph: Extendable>, + /// GraphQL response field instruments. + pub(crate) graphql: + Extendable>, } impl InstrumentsConfig { @@ -71,6 +78,8 @@ impl InstrumentsConfig { .defaults_for_levels(self.default_requirement_level, TelemetryDataKind::Metrics); self.subgraph .defaults_for_levels(self.default_requirement_level, TelemetryDataKind::Metrics); + self.graphql + .defaults_for_levels(self.default_requirement_level, TelemetryDataKind::Metrics); } pub(crate) fn new_router_instruments(&self) -> RouterInstruments { @@ -609,6 +618,7 @@ pub(crate) enum InstrumentType { pub(crate) enum InstrumentValue { Standard(Standard), Chunked(Event), + Field(Field), Custom(T), } @@ -634,6 +644,16 @@ pub(crate) enum Event { Custom(T), } +#[derive(Clone, Deserialize, JsonSchema, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum Field { + #[serde(rename = "field_unit")] + Unit, + /// For every field + #[serde(rename = "field_custom")] + Custom(T), +} + pub(crate) trait Instrumented { type Request; type Response; @@ -642,6 +662,7 @@ pub(crate) trait Instrumented { fn on_request(&self, request: &Self::Request); fn on_response(&self, response: &Self::Response); fn on_response_event(&self, _response: &Self::EventResponse, _ctx: &Context) {} + fn on_response_field(&self, _typed_value: &TypedValue, _ctx: &Context) {} fn on_error(&self, error: &BoxError, ctx: &Context); } @@ -670,6 +691,10 @@ where self.attributes.on_response_event(response, ctx); } + fn on_response_field(&self, typed_value: &TypedValue, ctx: &Context) { + self.attributes.on_response_field(typed_value, ctx); + } + fn on_error(&self, error: &BoxError, ctx: &Context) { self.attributes.on_error(error, ctx); } @@ -714,6 +739,16 @@ where histograms: Vec>, } +impl CustomInstruments +where + Attributes: Selectors + Default, + Select: Selector + Debug, +{ + pub(crate) fn is_empty(&self) -> bool { + self.counters.is_empty() && self.histograms.is_empty() + } +} + impl CustomInstruments where Attributes: Selectors + Default + Debug + Clone, @@ -746,6 +781,13 @@ where Increment::EventCustom(None), ), }, + InstrumentValue::Field(incr) => match incr { + Field::Unit => (None, Increment::FieldUnit), + Field::Custom(selector) => ( + Some(Arc::new(selector.clone())), + Increment::FieldCustom(None), + ), + }, }; let counter = CustomCounterInner { increment, @@ -759,7 +801,7 @@ where ), attributes: Vec::new(), selector, - selectors: instrument.attributes.clone(), + selectors: Some(instrument.attributes.clone()), incremented: false, }; @@ -787,6 +829,13 @@ where Increment::EventCustom(None), ), }, + InstrumentValue::Field(incr) => match incr { + Field::Unit => (None, Increment::FieldUnit), + Field::Custom(selector) => ( + Some(Arc::new(selector.clone())), + Increment::FieldCustom(None), + ), + }, }; let histogram = CustomHistogramInner { increment, @@ -864,6 +913,15 @@ where histogram.on_response_event(response, ctx); } } + + fn on_response_field(&self, typed_value: &TypedValue, ctx: &Context) { + for counter in &self.counters { + counter.on_response_field(typed_value, ctx); + } + for histogram in &self.histograms { + histogram.on_response_field(typed_value, ctx); + } + } } pub(crate) struct RouterInstruments { @@ -1056,33 +1114,35 @@ pub(crate) type SubgraphCustomInstruments = pub(crate) enum Increment { Unit, EventUnit, + FieldUnit, Duration(Instant), EventDuration(Instant), Custom(Option), EventCustom(Option), + FieldCustom(Option), } -struct CustomCounter +pub(crate) struct CustomCounter where A: Selectors + Default, T: Selector + Debug, { - inner: Mutex>, + pub(crate) inner: Mutex>, } -struct CustomCounterInner +pub(crate) struct CustomCounterInner where A: Selectors + Default, T: Selector + Debug, { - increment: Increment, - selector: Option>, - selectors: Arc>, - counter: Option>, - condition: Condition, - attributes: Vec, + pub(crate) increment: Increment, + pub(crate) selector: Option>, + pub(crate) selectors: Option>>, + pub(crate) counter: Option>, + pub(crate) condition: Condition, + pub(crate) attributes: Vec, // Useful when it's a counter on events to know if we have to count for an event or not - incremented: bool, + pub(crate) incremented: bool, } impl Instrumented for CustomCounter @@ -1102,7 +1162,10 @@ where let _ = inner.counter.take(); return; } - inner.attributes = inner.selectors.on_request(request).into_iter().collect(); + if let Some(selectors) = inner.selectors.as_ref() { + inner.attributes = selectors.on_request(request).into_iter().collect(); + } + if let Some(selected_value) = inner.selector.as_ref().and_then(|s| s.on_request(request)) { let new_incr = match &inner.increment { Increment::EventCustom(None) => { @@ -1125,13 +1188,22 @@ where if !inner.condition.evaluate_response(response) { if !matches!( &inner.increment, - Increment::EventCustom(_) | Increment::EventDuration(_) | Increment::EventUnit + Increment::EventCustom(_) + | Increment::EventDuration(_) + | Increment::EventUnit + | Increment::FieldCustom(_) + | Increment::FieldUnit ) { let _ = inner.counter.take(); } return; } - let attrs: Vec = inner.selectors.on_response(response).into_iter().collect(); + + let attrs: Vec = inner + .selectors + .as_ref() + .map(|s| s.on_response(response).into_iter().collect()) + .unwrap_or_default(); inner.attributes.extend(attrs); if let Some(selected_value) = inner @@ -1161,8 +1233,12 @@ where Some(incr) => incr as f64, None => 0f64, }, - Increment::EventUnit | Increment::EventDuration(_) | Increment::EventCustom(_) => { - // Nothing to do because we're incrementing on events + Increment::EventUnit + | Increment::EventDuration(_) + | Increment::EventCustom(_) + | Increment::FieldUnit + | Increment::FieldCustom(_) => { + // Nothing to do because we're incrementing on events or fields return; } }; @@ -1180,12 +1256,16 @@ where if !inner.condition.evaluate_event_response(response, ctx) { return; } - let attrs: Vec = inner - .selectors - .on_response_event(response, ctx) - .into_iter() - .collect(); - inner.attributes.extend(attrs); + // Response event may be called multiple times so we don't extend inner.attributes + let mut attrs = inner.attributes.clone(); + if let Some(selectors) = inner.selectors.as_ref() { + attrs.extend( + selectors + .on_response_event(response, ctx) + .into_iter() + .collect::>(), + ); + } if let Some(selected_value) = inner .selector @@ -1229,30 +1309,96 @@ where inner.incremented = true; if let Some(counter) = &inner.counter { - counter.add(increment, &inner.attributes); + counter.add(increment, &attrs); } } fn on_error(&self, error: &BoxError, _ctx: &Context) { let mut inner = self.inner.lock(); - let mut attrs: Vec = inner.selectors.on_error(error).into_iter().collect(); - attrs.append(&mut inner.attributes); + + let mut attrs = inner.attributes.clone(); + if let Some(selectors) = inner.selectors.as_ref() { + attrs.extend(selectors.on_error(error).into_iter().collect::>()); + } let increment = match inner.increment { - Increment::Unit | Increment::EventUnit => 1f64, + Increment::Unit | Increment::EventUnit | Increment::FieldUnit => 1f64, Increment::Duration(instant) | Increment::EventDuration(instant) => { instant.elapsed().as_secs_f64() } - Increment::Custom(val) | Increment::EventCustom(val) => match val { - Some(incr) => incr as f64, - None => 0f64, - }, + Increment::Custom(val) | Increment::EventCustom(val) | Increment::FieldCustom(val) => { + match val { + Some(incr) => incr as f64, + None => 0f64, + } + } }; if let Some(counter) = inner.counter.take() { counter.add(increment, &attrs); } } + + fn on_response_field(&self, typed_value: &TypedValue, ctx: &Context) { + let mut inner = self.inner.lock(); + if !inner.condition.evaluate_response_field(typed_value, ctx) { + return; + } + + // Response field may be called multiple times so we don't extend inner.attributes + let mut attrs = inner.attributes.clone(); + if let Some(selectors) = inner.selectors.as_ref() { + attrs.extend( + selectors + .on_response_field(typed_value, ctx) + .into_iter() + .collect::>(), + ); + } + + if let Some(selected_value) = inner + .selector + .as_ref() + .and_then(|s| s.on_response_field(typed_value, ctx)) + { + let new_incr = match &inner.increment { + Increment::FieldCustom(None) => { + Increment::FieldCustom(selected_value.as_str().parse::().ok()) + } + Increment::Custom(None) => { + Increment::FieldCustom(selected_value.as_str().parse::().ok()) + } + other => { + failfast_error!("this is a bug and should not happen, the increment should only be Custom or FieldCustom, please open an issue: {other:?}"); + return; + } + }; + inner.increment = new_incr; + } + + let increment: Option = match &mut inner.increment { + Increment::FieldUnit => Some(1f64), + Increment::FieldCustom(val) => { + let incr = val.map(|incr| incr as f64); + // Set it to None again + *val = None; + incr + } + Increment::Unit + | Increment::Duration(_) + | Increment::Custom(_) + | Increment::EventDuration(_) + | Increment::EventCustom(_) + | Increment::EventUnit => { + // Nothing to do because we're incrementing on fields + return; + } + }; + + if let (Some(counter), Some(increment)) = (&inner.counter, increment) { + counter.add(increment, &attrs); + } + } } impl Drop for CustomCounter @@ -1264,7 +1410,8 @@ where // TODO add attribute error broken pipe ? cf https://github.com/apollographql/router/issues/4866 let inner = self.inner.try_lock(); if let Some(mut inner) = inner { - if inner.incremented { + // If the condition is false or indeterminate then we don't increment the counter + if inner.incremented || matches!(inner.condition.evaluate_drop(), Some(false) | None) { return; } if let Some(counter) = inner.counter.take() { @@ -1277,6 +1424,13 @@ where Some(incr) => *incr as f64, None => 0f64, }, + Increment::FieldUnit | Increment::FieldCustom(_) => { + // Dropping a metric on a field will never increment. + // We can't increment graphql metrics unless we actually process the result. + // It's not like we're counting the number of requests, where we want to increment + // with the data that we know so far if the request stops. + return; + } }; counter.add(incr, &inner.attributes); } @@ -1415,6 +1569,9 @@ where Increment::EventCustom(None) => { Increment::EventCustom(selected_value.as_str().parse::().ok()) } + Increment::FieldCustom(None) => { + Increment::FieldCustom(selected_value.as_str().parse::().ok()) + } Increment::Custom(None) => { Increment::Custom(selected_value.as_str().parse::().ok()) } @@ -1432,7 +1589,11 @@ where if !inner.condition.evaluate_response(response) { if !matches!( &inner.increment, - Increment::EventCustom(_) | Increment::EventDuration(_) | Increment::EventUnit + Increment::EventCustom(_) + | Increment::EventDuration(_) + | Increment::EventUnit + | Increment::FieldCustom(_) + | Increment::FieldUnit ) { let _ = inner.histogram.take(); } @@ -1453,6 +1614,9 @@ where Increment::EventCustom(None) => { Increment::EventCustom(selected_value.as_str().parse::().ok()) } + Increment::FieldCustom(None) => { + Increment::FieldCustom(selected_value.as_str().parse::().ok()) + } Increment::Custom(None) => { Increment::Custom(selected_value.as_str().parse::().ok()) } @@ -1468,14 +1632,19 @@ where Increment::Unit => Some(1f64), Increment::Duration(instant) => Some(instant.elapsed().as_secs_f64()), Increment::Custom(val) => val.map(|incr| incr as f64), - Increment::EventUnit | Increment::EventDuration(_) | Increment::EventCustom(_) => { - // Nothing to do because we're incrementing on events + Increment::EventUnit + | Increment::EventDuration(_) + | Increment::EventCustom(_) + | Increment::FieldUnit + | Increment::FieldCustom(_) => { + // Nothing to do because we're incrementing on events or fields return; } }; - if let (Some(histogram), Some(increment)) = (inner.histogram.take(), increment) { + if let (Some(histogram), Some(increment)) = (&inner.histogram, increment) { histogram.record(increment, &inner.attributes); + inner.updated = true; } } @@ -1484,12 +1653,18 @@ where if !inner.condition.evaluate_event_response(response, ctx) { return; } - let mut attrs: Vec = inner - .selectors - .as_ref() - .map(|s| s.on_response_event(response, ctx).into_iter().collect()) - .unwrap_or_default(); - attrs.extend(inner.attributes.clone()); + + // Response event may be called multiple times so we don't extend inner.attributes + let mut attrs: Vec = inner.attributes.clone(); + if let Some(selectors) = inner.selectors.as_ref() { + attrs.extend( + selectors + .on_response_event(response, ctx) + .into_iter() + .collect::>(), + ); + } + if let Some(selected_value) = inner .selector .as_ref() @@ -1524,14 +1699,18 @@ where *val = None; incr } - Increment::Unit | Increment::Duration(_) | Increment::Custom(_) => { + Increment::Unit + | Increment::Duration(_) + | Increment::Custom(_) + | Increment::FieldUnit + | Increment::FieldCustom(_) => { // Nothing to do because we're incrementing on events return; } }; - inner.updated = true; if let (Some(histogram), Some(increment)) = (&inner.histogram, increment) { histogram.record(increment, &attrs); + inner.updated = true; } } @@ -1545,17 +1724,80 @@ where attrs.append(&mut inner.attributes); let increment = match inner.increment { - Increment::Unit | Increment::EventUnit => Some(1f64), + Increment::Unit | Increment::EventUnit | Increment::FieldUnit => Some(1f64), Increment::Duration(instant) | Increment::EventDuration(instant) => { Some(instant.elapsed().as_secs_f64()) } - Increment::Custom(val) | Increment::EventCustom(val) => val.map(|incr| incr as f64), + Increment::Custom(val) | Increment::EventCustom(val) | Increment::FieldCustom(val) => { + val.map(|incr| incr as f64) + } }; if let (Some(histogram), Some(increment)) = (inner.histogram.take(), increment) { histogram.record(increment, &attrs); } } + + fn on_response_field(&self, typed_value: &TypedValue, ctx: &Context) { + let mut inner = self.inner.lock(); + if !inner.condition.evaluate_response_field(typed_value, ctx) { + return; + } + + // Response field may be called multiple times so we don't extend inner.attributes + let mut attrs = inner.attributes.clone(); + if let Some(selectors) = inner.selectors.as_ref() { + attrs.extend( + selectors + .on_response_field(typed_value, ctx) + .into_iter() + .collect::>(), + ); + } + + if let Some(selected_value) = inner + .selector + .as_ref() + .and_then(|s| s.on_response_field(typed_value, ctx)) + { + let new_incr = match &inner.increment { + Increment::FieldCustom(None) => { + Increment::FieldCustom(selected_value.as_str().parse::().ok()) + } + Increment::Custom(None) => { + Increment::FieldCustom(selected_value.as_str().parse::().ok()) + } + other => { + failfast_error!("this is a bug and should not happen, the increment should only be Custom or FieldCustom, please open an issue: {other:?}"); + return; + } + }; + inner.increment = new_incr; + } + + let increment: Option = match &mut inner.increment { + Increment::FieldUnit => Some(1f64), + Increment::FieldCustom(val) => { + let incr = val.map(|incr| incr as f64); + // Set it to None again + *val = None; + incr + } + Increment::Unit + | Increment::Duration(_) + | Increment::Custom(_) + | Increment::EventDuration(_) + | Increment::EventCustom(_) + | Increment::EventUnit => { + // Nothing to do because we're incrementing on fields + return; + } + }; + + if let (Some(histogram), Some(increment)) = (&inner.histogram, increment) { + histogram.record(increment, &attrs); + } + } } impl Drop for CustomHistogram @@ -1567,7 +1809,7 @@ where // TODO add attribute error broken pipe ? cf https://github.com/apollographql/router/issues/4866 let inner = self.inner.try_lock(); if let Some(mut inner) = inner { - if inner.updated { + if inner.updated || matches!(inner.condition.evaluate_drop(), Some(false) | None) { return; } if let Some(histogram) = inner.histogram.take() { @@ -1579,6 +1821,13 @@ where Increment::Custom(val) | Increment::EventCustom(val) => { val.map(|incr| incr as f64) } + Increment::FieldUnit | Increment::FieldCustom(_) => { + // Dropping a metric on a field will never increment. + // We can't increment graphql metrics unless we actually process the result. + // It's not like we're counting the number of requests, where we want to increment + // with the data that we know so far if the request stops. + return; + } }; if let Some(increment) = increment { @@ -1591,18 +1840,660 @@ where #[cfg(test)] mod tests { + use std::fs::File; + use std::io::Write; + use std::path::PathBuf; + use std::str::FromStr; + + use apollo_compiler::ast::Name; + use apollo_compiler::ast::NamedType; + use apollo_compiler::executable::SelectionSet; + use apollo_compiler::execution::JsonMap; + use http::HeaderMap; + use http::HeaderName; + use http::Method; use http::StatusCode; + use http::Uri; + use multimap::MultiMap; + use rust_embed::RustEmbed; + use schemars::gen::SchemaGenerator; + use serde::Deserialize; use serde_json::json; + use serde_json_bytes::Value; use super::*; + use crate::context::CONTAINS_GRAPHQL_ERROR; use crate::context::OPERATION_KIND; + use crate::error::Error; use crate::graphql; + use crate::http_ext::TryIntoHeaderName; + use crate::http_ext::TryIntoHeaderValue; + use crate::json_ext::Path; use crate::metrics::FutureMetricsExt; + use crate::plugins::telemetry::config_new::graphql::GraphQLInstruments; + use crate::plugins::telemetry::config_new::instruments::Instrumented; + use crate::plugins::telemetry::config_new::instruments::InstrumentsConfig; + use crate::services::OperationKind; use crate::services::RouterRequest; use crate::services::RouterResponse; + use crate::Context; + + #[derive(RustEmbed)] + #[folder = "src/plugins/telemetry/config_new/fixtures"] + struct Asset; + + #[derive(Deserialize, JsonSchema)] + #[serde(rename_all = "snake_case", deny_unknown_fields)] + enum Event { + Context { + map: serde_json::Map, + }, + RouterRequest { + method: String, + uri: String, + #[serde(default)] + headers: HashMap, + body: String, + }, + RouterResponse { + status: u16, + #[serde(default)] + headers: HashMap, + body: String, + }, + RouterError { + error: String, + }, + SupergraphRequest { + query: String, + method: String, + uri: String, + #[serde(default)] + headers: HashMap, + }, + SupergraphResponse { + status: u16, + #[serde(default)] + headers: HashMap, + label: Option, + #[schemars(with = "Option")] + data: Option, + #[schemars(with = "Option")] + path: Option, + #[serde(default)] + #[schemars(with = "Vec")] + errors: Vec, + // Skip the `Object` type alias in order to use buildstructor’s map special-casing + #[serde(default)] + #[schemars(with = "Option>")] + extensions: JsonMap, + }, + SubgraphRequest { + subgraph_name: String, + operation_kind: Option, + query: String, + operation_name: Option, + #[serde(default)] + #[schemars(with = "Option>")] + variables: JsonMap, + #[serde(default)] + #[schemars(with = "Option>")] + extensions: JsonMap, + #[serde(default)] + headers: HashMap, + }, + SupergraphError { + error: String, + }, + SubgraphResponse { + status: u16, + data: Option, + #[serde(default)] + #[schemars(with = "Option>")] + extensions: JsonMap, + #[serde(default)] + #[schemars(with = "Vec")] + errors: Vec, + #[serde(default)] + headers: HashMap, + }, + /// Note that this MUST not be used without first using supergraph request event + GraphqlResponse { + #[schemars(with = "Option")] + data: Option, + #[schemars(with = "Option")] + path: Option, + #[serde(default)] + #[schemars(with = "Vec")] + errors: Vec, + // Skip the `Object` type alias in order to use buildstructor’s map special-casing + #[serde(default)] + #[schemars(with = "Option>")] + extensions: JsonMap, + }, + /// Note that this MUST not be used without first using supergraph request event + ResponseField { + typed_value: TypedValueMirror, + }, + } + + #[derive(Deserialize, JsonSchema)] + #[serde(rename_all = "snake_case", deny_unknown_fields)] + enum TypedValueMirror { + Null, + Bool { + type_name: String, + field_name: String, + field_type: String, + value: bool, + }, + Number { + type_name: String, + field_name: String, + field_type: String, + value: serde_json::Number, + }, + String { + type_name: String, + field_name: String, + field_type: String, + value: String, + }, + List { + type_name: String, + field_name: String, + field_type: String, + values: Vec, + }, + Object { + type_name: String, + field_name: String, + field_type: String, + values: HashMap, + }, + Root { + values: HashMap, + }, + } + + #[derive(Deserialize, JsonSchema)] + #[serde(deny_unknown_fields, rename_all = "snake_case")] + struct TestDefinition { + description: String, + events: Vec>, + } + + #[tokio::test] + async fn test_instruments() { + // This test is data driven. + // It reads a list of fixtures from the fixtures directory and runs a test for each fixture. + // Each fixture is a yaml file that contains a list of events and a router config for the instruments. + + for fixture in Asset::iter() { + // There's no async in this test, but introducing an async block allows us to separate metrics for each fixture. + async move { + if fixture.ends_with("test.yaml") { + println!("Running test for fixture: {}", fixture); + let path = PathBuf::from_str(&fixture).unwrap(); + let fixture_name = path + .parent() + .expect("fixture path") + .file_name() + .expect("fixture name"); + let test_definition_file = Asset::get(&fixture).expect("failed to get fixture"); + let test_definition: TestDefinition = + serde_yaml::from_slice(&test_definition_file.data) + .expect("failed to parse fixture"); + + let router_config_file = + Asset::get(&fixture.replace("test.yaml", "router.yaml")) + .expect("failed to get fixture router config"); + + let mut config = load_config(&router_config_file.data); + config.update_defaults(); + + for request in test_definition.events { + // each array of actions is a separate request + let mut router_instruments = None; + let mut supergraph_instruments = None; + let mut subgraph_instruments = None; + let graphql_instruments: GraphQLInstruments = (&config).into(); + let context = Context::new(); + for event in request { + match event { + Event::RouterRequest { + method, + uri, + headers, + body, + } => { + let router_req = RouterRequest::fake_builder() + .context(context.clone()) + .method(Method::from_str(&method).expect("method")) + .uri(Uri::from_str(&uri).expect("uri")) + .headers(convert_headers(headers)) + .body(body) + .build() + .unwrap(); + router_instruments = Some(config.new_router_instruments()); + router_instruments + .as_mut() + .expect("router instruments") + .on_request(&router_req); + } + Event::RouterResponse { + status, + headers, + body, + } => { + let router_resp = RouterResponse::fake_builder() + .context(context.clone()) + .status_code(StatusCode::from_u16(status).expect("status")) + .headers(convert_headers(headers)) + .data(body) + .build() + .unwrap(); + router_instruments + .take() + .expect("router instruments") + .on_response(&router_resp); + } + Event::RouterError { error } => { + router_instruments + .take() + .expect("router request must have been made first") + .on_error(&BoxError::from(error), &context); + } + Event::SupergraphRequest { + query, + method, + uri, + headers, + } => { + supergraph_instruments = + Some(config.new_supergraph_instruments()); + + let mut request = supergraph::Request::fake_builder() + .context(context.clone()) + .method(Method::from_str(&method).expect("method")) + .headers(convert_headers(headers)) + .query(query) + .build() + .unwrap(); + *request.supergraph_request.uri_mut() = + Uri::from_str(&uri).expect("uri"); + + supergraph_instruments + .as_mut() + .unwrap() + .on_request(&request); + } + Event::SupergraphResponse { + status, + label, + data, + path, + errors, + extensions, + headers, + } => { + let response = supergraph::Response::fake_builder() + .context(context.clone()) + .status_code(StatusCode::from_u16(status).expect("status")) + .and_label(label) + .and_path(path) + .errors(errors) + .extensions(extensions) + .and_data(data) + .headers(convert_headers(headers)) + .build() + .unwrap(); + + supergraph_instruments + .take() + .unwrap() + .on_response(&response); + } + Event::SubgraphRequest { + subgraph_name, + operation_kind, + query, + operation_name, + variables, + extensions, + headers, + } => { + subgraph_instruments = Some(config.new_subgraph_instruments()); + let graphql_request = graphql::Request::fake_builder() + .query(query) + .and_operation_name(operation_name) + .variables(variables) + .extensions(extensions) + .build(); + let mut http_request = http::Request::new(graphql_request); + *http_request.headers_mut() = convert_http_headers(headers); + + let request = subgraph::Request::fake_builder() + .context(context.clone()) + .subgraph_name(subgraph_name) + .and_operation_kind(operation_kind) + .subgraph_request(http_request) + .build(); + + subgraph_instruments.as_mut().unwrap().on_request(&request); + } + Event::SubgraphResponse { + status, + data, + extensions, + errors, + headers, + } => { + let response = subgraph::Response::fake2_builder() + .context(context.clone()) + .status_code(StatusCode::from_u16(status).expect("status")) + .and_data(data) + .errors(errors) + .extensions(extensions) + .headers(convert_headers(headers)) + .build() + .unwrap(); + subgraph_instruments + .take() + .expect("subgraph request must have been made first") + .on_response(&response); + } + Event::SupergraphError { error } => { + supergraph_instruments + .take() + .expect("supergraph request must have been made first") + .on_error(&BoxError::from(error), &context); + } + Event::GraphqlResponse { + data, + path, + errors, + extensions, + } => { + let response = graphql::Response::builder() + .and_data(data) + .and_path(path) + .errors(errors) + .extensions(extensions) + .build(); + supergraph_instruments + .as_mut() + .expect( + "supergraph request event should have happened first", + ) + .on_response_event(&response, &context); + } + Event::ResponseField { typed_value } => { + let typed_value_data: TypedValueData = typed_value.into(); + let typed_value = TypedValue::from(&typed_value_data); + graphql_instruments.on_response_field(&typed_value, &context); + } + Event::Context { map } => { + for (key, value) in map { + context.insert(key, value).expect("insert context"); + } + } + } + } + } + + let mut snapshot_path = PathBuf::new(); + snapshot_path.push("fixtures"); + path.iter().for_each(|p| snapshot_path.push(p)); + snapshot_path.pop(); + let description = test_definition.description; + let info: serde_yaml::Value = serde_yaml::from_slice(&router_config_file.data) + .expect("failed to parse fixture"); + + insta::with_settings!({sort_maps => true, + snapshot_path=>snapshot_path, + input_file=>fixture_name, + prepend_module_to_snapshot=>false, + description=>description, + info=>&info + }, { + let metrics = crate::metrics::collect_metrics(); + insta::assert_yaml_snapshot!("metrics", &metrics.all()); + }); + } + } + .with_metrics() + .await; + } + } + + fn convert_http_headers(headers: HashMap) -> HeaderMap { + let mut converted_headers = HeaderMap::new(); + for (name, value) in headers { + converted_headers.insert::( + name.try_into().expect("expected header name"), + value.try_into().expect("expected header value"), + ); + } + converted_headers + } + + fn convert_headers( + headers: HashMap, + ) -> MultiMap { + let mut converted_headers: MultiMap = + MultiMap::new(); + for (name, value) in headers { + converted_headers.insert(name.into(), value.into()); + } + converted_headers + } + + fn load_config(config: &[u8]) -> InstrumentsConfig { + let val: serde_json::Value = serde_yaml::from_slice(config).unwrap(); + let instruments = val + .as_object() + .unwrap() + .get("telemetry") + .unwrap() + .as_object() + .unwrap() + .get("instrumentation") + .unwrap() + .as_object() + .unwrap() + .get("instruments") + .unwrap(); + serde_json::from_value(instruments.clone()).unwrap() + } + + #[test] + fn write_schema() { + // Write a json schema for the above test + let mut schema_gen = SchemaGenerator::default(); + let schema = schema_gen.root_schema_for::(); + let schema = serde_json::to_string_pretty(&schema); + let mut path = PathBuf::from_str(env!("CARGO_MANIFEST_DIR")).expect("manifest dir"); + path.push("src"); + path.push("plugins"); + path.push("telemetry"); + path.push("config_new"); + path.push("fixtures"); + path.push("schema.json"); + let mut file = File::create(path).unwrap(); + file.write_all(schema.unwrap().as_bytes()) + .expect("write schema"); + } + + enum TypedValueData { + Null, + Bool { + type_name: NamedType, + field_definition: apollo_compiler::executable::Field, + value: bool, + }, + Number { + type_name: NamedType, + field_definition: apollo_compiler::executable::Field, + value: serde_json::Number, + }, + String { + type_name: NamedType, + field_definition: apollo_compiler::executable::Field, + value: String, + }, + List { + type_name: NamedType, + field_definition: apollo_compiler::executable::Field, + values: Vec, + }, + Object { + type_name: NamedType, + field_definition: apollo_compiler::executable::Field, + values: HashMap, + }, + Root { + values: HashMap, + }, + } + + impl From for TypedValueData { + fn from(value: TypedValueMirror) -> Self { + match value { + TypedValueMirror::Null => TypedValueData::Null, + TypedValueMirror::Bool { + type_name, + field_type, + field_name, + value, + } => TypedValueData::Bool { + type_name: Self::type_name(type_name), + field_definition: Self::field(field_type, field_name), + value, + }, + TypedValueMirror::Number { + type_name, + field_type, + field_name, + value, + } => TypedValueData::Number { + type_name: Self::type_name(type_name), + field_definition: Self::field(field_type, field_name), + value, + }, + TypedValueMirror::String { + type_name, + field_type, + field_name, + value, + } => TypedValueData::String { + type_name: Self::type_name(type_name), + field_definition: Self::field(field_type, field_name), + value, + }, + TypedValueMirror::List { + type_name, + field_type, + field_name, + values, + } => TypedValueData::List { + type_name: Self::type_name(type_name), + field_definition: Self::field(field_type, field_name), + values: values.into_iter().map(|v| v.into()).collect(), + }, + TypedValueMirror::Object { + type_name, + field_type, + field_name, + values, + } => TypedValueData::Object { + type_name: Self::type_name(type_name), + field_definition: Self::field(field_type, field_name), + values: values.into_iter().map(|(k, v)| (k, v.into())).collect(), + }, + TypedValueMirror::Root { values } => TypedValueData::Root { + values: values.into_iter().map(|(k, v)| (k, v.into())).collect(), + }, + } + } + } + + impl TypedValueData { + fn field(field_type: String, field_name: String) -> apollo_compiler::executable::Field { + apollo_compiler::executable::Field { + definition: apollo_compiler::schema::FieldDefinition { + description: None, + name: NamedType::new(field_name.clone()).expect("valid field name"), + arguments: vec![], + ty: apollo_compiler::schema::Type::Named( + NamedType::new(field_type.clone()).expect("valid type name"), + ), + directives: Default::default(), + } + .into(), + alias: None, + name: NamedType::new(field_name.clone()).expect("valid field name"), + arguments: vec![], + directives: Default::default(), + selection_set: SelectionSet::new( + NamedType::new(field_name).expect("valid field name"), + ), + } + } + + fn type_name(type_name: String) -> Name { + NamedType::new(type_name).expect("valid type name") + } + } + + impl<'a> From<&'a TypedValueData> for TypedValue<'a> { + fn from(value: &'a TypedValueData) -> Self { + match value { + TypedValueData::Null => TypedValue::Null, + TypedValueData::Bool { + type_name, + field_definition, + value, + } => TypedValue::Bool(type_name, field_definition, value), + TypedValueData::Number { + type_name, + field_definition, + value, + } => TypedValue::Number(type_name, field_definition, value), + TypedValueData::String { + type_name, + field_definition, + value, + } => TypedValue::String(type_name, field_definition, value), + TypedValueData::List { + type_name, + field_definition, + values, + } => TypedValue::List( + type_name, + field_definition, + values.iter().map(|v| v.into()).collect(), + ), + TypedValueData::Object { + type_name, + field_definition, + values, + } => TypedValue::Object( + type_name, + field_definition, + values.iter().map(|(k, v)| (k.clone(), v.into())).collect(), + ), + TypedValueData::Root { values } => { + TypedValue::Root(values.iter().map(|(k, v)| (k.clone(), v.into())).collect()) + } + } + } + } #[tokio::test] async fn test_router_instruments() { + // Please don't add further logic to this test, it's already testing multiple things. + // Instead, add a data driven test via test_instruments test. async { let config: InstrumentsConfig = serde_json::from_str( json!({ @@ -1815,6 +2706,8 @@ mod tests { #[tokio::test] async fn test_supergraph_instruments() { + // Please don't add further logic to this test, it's already testing multiple things. + // Instead, add a data driven test via test_instruments test. async { let config: InstrumentsConfig = serde_json::from_str( json!({ @@ -1854,6 +2747,44 @@ mod tests { } } }, + "acme.request.on_graphql_error_selector": { + "value": "event_unit", + "type": "counter", + "unit": "error", + "description": "my description", + "condition": { + "eq": [ + true, + { + "on_graphql_error": true + } + ] + }, + "attributes": { + "response_errors": { + "response_errors": "$.*" + } + } + }, + "acme.request.on_graphql_error_histo": { + "value": "event_unit", + "type": "histogram", + "unit": "error", + "description": "my description", + "condition": { + "eq": [ + "NOPE", + { + "response_errors": "$.[0].extensions.code" + } + ] + }, + "attributes": { + "response_errors": { + "response_errors": "$.*" + } + } + }, "acme.request.on_graphql_data": { "value": { "response_data": "$.price" @@ -1895,7 +2826,14 @@ mod tests { let custom_instruments = SupergraphCustomInstruments::new(&config.supergraph.custom); let context = crate::context::Context::new(); - let _ = context.insert(OPERATION_KIND, "query".to_string()); + let _ = context.insert(OPERATION_KIND, "query".to_string()).unwrap(); + let context_with_error = crate::context::Context::new(); + let _ = context_with_error + .insert(OPERATION_KIND, "query".to_string()) + .unwrap(); + let _ = context_with_error + .insert(CONTAINS_GRAPHQL_ERROR, true) + .unwrap(); let supergraph_req = supergraph::Request::fake_builder() .header("conditional-custom", "X") .header("x-my-header-count", "55") @@ -1929,7 +2867,7 @@ mod tests { .extension_code("NOPE") .build()]) .build(), - &supergraph_req.context.clone(), + &context_with_error, ); assert_counter!("acme.query", 1.0, query = "{me{name}}"); @@ -1939,6 +2877,16 @@ mod tests { 1.0, response_errors = "{\"message\":\"nope\",\"extensions\":{\"code\":\"NOPE\"}}" ); + assert_counter!( + "acme.request.on_graphql_error_selector", + 1.0, + response_errors = "{\"message\":\"nope\",\"extensions\":{\"code\":\"NOPE\"}}" + ); + assert_histogram_sum!( + "acme.request.on_graphql_error_histo", + 1.0, + response_errors = "{\"message\":\"nope\",\"extensions\":{\"code\":\"NOPE\"}}" + ); assert_counter!("acme.request.on_graphql_data", 500.0, response.data = 500); let custom_instruments = SupergraphCustomInstruments::new(&config.supergraph.custom); @@ -1973,7 +2921,7 @@ mod tests { .extension_code("NOPE") .build()]) .build(), - &supergraph_req.context.clone(), + &context_with_error, ); assert_counter!("acme.query", 1.0, query = "{me{name}}"); @@ -1983,6 +2931,16 @@ mod tests { 2.0, response_errors = "{\"message\":\"nope\",\"extensions\":{\"code\":\"NOPE\"}}" ); + assert_counter!( + "acme.request.on_graphql_error_selector", + 2.0, + response_errors = "{\"message\":\"nope\",\"extensions\":{\"code\":\"NOPE\"}}" + ); + assert_histogram_sum!( + "acme.request.on_graphql_error_histo", + 2.0, + response_errors = "{\"message\":\"nope\",\"extensions\":{\"code\":\"NOPE\"}}" + ); assert_counter!("acme.request.on_graphql_data", 1000.0, response.data = 500); let custom_instruments = SupergraphCustomInstruments::new(&config.supergraph.custom); @@ -2007,7 +2965,7 @@ mod tests { &graphql::Response::builder() .data(serde_json_bytes::json!({"foo": "bar"})) .build(), - &supergraph_req.context.clone(), + &supergraph_req.context, ); assert_counter!("acme.query", 2.0, query = "{me{name}}"); @@ -2017,6 +2975,16 @@ mod tests { 2.0, response_errors = "{\"message\":\"nope\",\"extensions\":{\"code\":\"NOPE\"}}" ); + assert_counter!( + "acme.request.on_graphql_error_selector", + 2.0, + response_errors = "{\"message\":\"nope\",\"extensions\":{\"code\":\"NOPE\"}}" + ); + assert_histogram_sum!( + "acme.request.on_graphql_error_histo", + 2.0, + response_errors = "{\"message\":\"nope\",\"extensions\":{\"code\":\"NOPE\"}}" + ); assert_counter!("acme.request.on_graphql_data", 1000.0, response.data = 500); } .with_metrics() diff --git a/apollo-router/src/plugins/telemetry/config_new/mod.rs b/apollo-router/src/plugins/telemetry/config_new/mod.rs index 3840336def..3ca9578803 100644 --- a/apollo-router/src/plugins/telemetry/config_new/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/mod.rs @@ -2,12 +2,14 @@ use opentelemetry::baggage::BaggageExt; use opentelemetry::trace::TraceContextExt; use opentelemetry::trace::TraceId; use opentelemetry::KeyValue; +use opentelemetry_api::Value; use paste::paste; use tower::BoxError; use tracing::Span; use super::otel::OpenTelemetrySpanExt; use super::otlp::TelemetryDataKind; +use crate::plugins::demand_control::cost_calculator::schema_aware_response::TypedValue; use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel; use crate::Context; @@ -21,6 +23,7 @@ mod cost; pub(crate) mod events; mod experimental_when_header; pub(crate) mod extendable; +pub(crate) mod graphql; pub(crate) mod instruments; pub(crate) mod logging; pub(crate) mod selectors; @@ -37,6 +40,9 @@ pub(crate) trait Selectors { Vec::with_capacity(0) } fn on_error(&self, error: &BoxError) -> Vec; + fn on_response_field(&self, _typed_value: &TypedValue, _ctx: &Context) -> Vec { + Vec::with_capacity(0) + } } pub(crate) trait Selector { @@ -54,6 +60,17 @@ pub(crate) trait Selector { None } fn on_error(&self, error: &BoxError) -> Option; + fn on_response_field( + &self, + _typed_value: &TypedValue, + _ctx: &Context, + ) -> Option { + None + } + + fn on_drop(&self) -> Option { + None + } } pub(crate) trait DefaultForLevel { @@ -186,6 +203,14 @@ impl From for AttributeValue { #[cfg(test)] mod test { + use std::sync::OnceLock; + + use apollo_compiler::ast::FieldDefinition; + use apollo_compiler::ast::NamedType; + use apollo_compiler::ast::Type; + use apollo_compiler::executable::Field; + use apollo_compiler::Node; + use apollo_compiler::NodeStr; use opentelemetry::trace::SpanContext; use opentelemetry::trace::SpanId; use opentelemetry::trace::TraceContextExt; @@ -203,6 +228,28 @@ mod test { use crate::plugins::telemetry::config_new::ToOtelValue; use crate::plugins::telemetry::otel; + pub(crate) fn field() -> &'static Field { + static FIELD: OnceLock = OnceLock::new(); + FIELD.get_or_init(|| { + Field::new( + NamedType::new_unchecked(NodeStr::from_static(&"field_name")), + Node::new(FieldDefinition { + description: None, + name: NamedType::new_unchecked(NodeStr::from_static(&"field_name")), + arguments: vec![], + ty: Type::Named(NamedType::new_unchecked(NodeStr::from_static( + &"field_type", + ))), + directives: Default::default(), + }), + ) + }) + } + pub(crate) fn ty() -> &'static NamedType { + static TYPE: NamedType = NamedType::new_unchecked(NodeStr::from_static(&"type_name")); + &TYPE + } + #[test] fn dd_convert() { let trace_id = TraceId::from_hex("234e10d9e749a0a19e94ac0e4a896aee").unwrap(); diff --git a/apollo-router/src/plugins/telemetry/config_new/selectors.rs b/apollo-router/src/plugins/telemetry/config_new/selectors.rs index ad9c9b5edd..1316acf209 100644 --- a/apollo-router/src/plugins/telemetry/config_new/selectors.rs +++ b/apollo-router/src/plugins/telemetry/config_new/selectors.rs @@ -2,6 +2,7 @@ use access_json::JSONQuery; use derivative::Derivative; use jsonpath_rust::JsonPathFinder; use jsonpath_rust::JsonPathInst; +use opentelemetry_api::Value; use schemars::JsonSchema; use serde::Deserialize; use serde_json_bytes::ByteString; @@ -20,6 +21,7 @@ use crate::plugins::telemetry::config_new::trace_id; use crate::plugins::telemetry::config_new::DatadogId; use crate::plugins::telemetry::config_new::Selector; use crate::plugins::telemetry::config_new::ToOtelValue; +use crate::query_planner::APOLLO_OPERATION_ID; use crate::services::router; use crate::services::subgraph; use crate::services::supergraph; @@ -33,6 +35,8 @@ pub(crate) enum TraceIdFormat { OpenTelemetry, /// Datadog trace ID, a u64. Datadog, + /// Apollo Studio trace id + Apollo, } #[derive(Deserialize, JsonSchema, Clone, Debug)] @@ -156,10 +160,11 @@ pub(crate) enum RouterSelector { /// Optional default value. default: Option, }, + /// Deprecated, should not be used anymore, use static field instead Static(String), StaticField { - /// A static string value - r#static: String, + /// A static value + r#static: AttributeValue, }, OnGraphQLError { /// Boolean set to true if the response body contains graphql error @@ -306,10 +311,15 @@ pub(crate) enum SupergraphSelector { /// Optional default value. default: Option, }, + /// Deprecated, should not be used anymore, use static field instead Static(String), StaticField { - /// A static string value - r#static: String, + /// A static value + r#static: AttributeValue, + }, + OnGraphQLError { + /// Boolean set to true if the response body contains graphql error + on_graphql_error: bool, }, Error { #[allow(dead_code)] @@ -317,7 +327,6 @@ pub(crate) enum SupergraphSelector { error: ErrorRepr, }, /// Cost attributes - #[allow(dead_code)] Cost { /// The cost value to select, one of: estimated, actual, delta. cost: CostValue, @@ -518,10 +527,11 @@ pub(crate) enum SubgraphSelector { /// Optional default value. default: Option, }, + /// Deprecated, should not be used anymore, use static field instead Static(String), StaticField { - /// A static string value - r#static: String, + /// A static value + r#static: AttributeValue, }, Error { #[allow(dead_code)] @@ -556,13 +566,20 @@ impl Selector for RouterSelector { .map(opentelemetry::Value::from), RouterSelector::TraceId { trace_id: trace_id_format, - } => trace_id().map(|id| { - match trace_id_format { - TraceIdFormat::OpenTelemetry => id.to_string(), - TraceIdFormat::Datadog => id.to_datadog(), + } => { + if let TraceIdFormat::Apollo = &trace_id_format { + return None; } - .into() - }), + trace_id().map(|id| { + match trace_id_format { + TraceIdFormat::OpenTelemetry => id.to_string(), + TraceIdFormat::Datadog => id.to_datadog(), + // It happens in the response + TraceIdFormat::Apollo => String::new(), + } + .into() + }) + } RouterSelector::Baggage { baggage, default, .. } => get_baggage(baggage).or_else(|| default.maybe_to_otel_value()), @@ -617,7 +634,22 @@ impl Selector for RouterSelector { None } } + RouterSelector::Static(val) => Some(val.clone().into()), RouterSelector::StaticField { r#static } => Some(r#static.clone().into()), + RouterSelector::TraceId { + trace_id: trace_id_format, + } => { + if let TraceIdFormat::Apollo = &trace_id_format { + response + .context + .get::<_, String>(APOLLO_OPERATION_ID) + .ok() + .flatten() + .map(opentelemetry::Value::from) + } else { + None + } + } _ => None, } } @@ -625,6 +657,16 @@ impl Selector for RouterSelector { fn on_error(&self, error: &tower::BoxError) -> Option { match self { RouterSelector::Error { .. } => Some(error.to_string().into()), + RouterSelector::Static(val) => Some(val.clone().into()), + RouterSelector::StaticField { r#static } => Some(r#static.clone().into()), + _ => None, + } + } + + fn on_drop(&self) -> Option { + match self { + RouterSelector::Static(val) => Some(val.clone().into()), + RouterSelector::StaticField { r#static } => Some(r#static.clone().into()), _ => None, } } @@ -750,6 +792,16 @@ impl Selector for SupergraphSelector { .as_ref() .and_then(|v| v.maybe_to_otel_value()) .or_else(|| default.maybe_to_otel_value()), + SupergraphSelector::OnGraphQLError { on_graphql_error } if *on_graphql_error => { + if response.context.get_json_value(CONTAINS_GRAPHQL_ERROR) + == Some(serde_json_bytes::Value::Bool(true)) + { + Some(opentelemetry::Value::Bool(true)) + } else { + None + } + } + SupergraphSelector::Static(val) => Some(val.clone().into()), SupergraphSelector::StaticField { r#static } => Some(r#static.clone().into()), // For request _ => None, @@ -814,6 +866,17 @@ impl Selector for SupergraphSelector { CostValue::Result => cost_result.result.into(), }) } + SupergraphSelector::OnGraphQLError { on_graphql_error } if *on_graphql_error => { + if ctx.get_json_value(CONTAINS_GRAPHQL_ERROR) + == Some(serde_json_bytes::Value::Bool(true)) + { + Some(opentelemetry::Value::Bool(true)) + } else { + None + } + } + SupergraphSelector::Static(val) => Some(val.clone().into()), + SupergraphSelector::StaticField { r#static } => Some(r#static.clone().into()), _ => None, } } @@ -821,6 +884,16 @@ impl Selector for SupergraphSelector { fn on_error(&self, error: &tower::BoxError) -> Option { match self { SupergraphSelector::Error { .. } => Some(error.to_string().into()), + SupergraphSelector::Static(val) => Some(val.clone().into()), + SupergraphSelector::StaticField { r#static } => Some(r#static.clone().into()), + _ => None, + } + } + + fn on_drop(&self) -> Option { + match self { + SupergraphSelector::Static(val) => Some(val.clone().into()), + SupergraphSelector::StaticField { r#static } => Some(r#static.clone().into()), _ => None, } } @@ -1058,6 +1131,7 @@ impl Selector for SubgraphSelector { .as_ref() .and_then(|v| v.maybe_to_otel_value()) .or_else(|| default.maybe_to_otel_value()), + SubgraphSelector::Static(val) => Some(val.clone().into()), SubgraphSelector::StaticField { r#static } => Some(r#static.clone().into()), // For request _ => None, @@ -1067,6 +1141,16 @@ impl Selector for SubgraphSelector { fn on_error(&self, error: &tower::BoxError) -> Option { match self { SubgraphSelector::Error { .. } => Some(error.to_string().into()), + SubgraphSelector::Static(val) => Some(val.clone().into()), + SubgraphSelector::StaticField { r#static } => Some(r#static.clone().into()), + _ => None, + } + } + + fn on_drop(&self) -> Option { + match self { + SubgraphSelector::Static(val) => Some(val.clone().into()), + SubgraphSelector::StaticField { r#static } => Some(r#static.clone().into()), _ => None, } } @@ -1108,6 +1192,7 @@ mod test { use crate::plugins::telemetry::config_new::selectors::TraceIdFormat; use crate::plugins::telemetry::config_new::Selector; use crate::plugins::telemetry::otel; + use crate::query_planner::APOLLO_OPERATION_ID; #[test] fn router_static() { @@ -1122,6 +1207,25 @@ mod test { .unwrap(), "test_static".into() ); + assert_eq!(selector.on_drop().unwrap(), "test_static".into()); + } + + #[test] + fn router_static_field() { + let selector = RouterSelector::StaticField { + r#static: "test_static".to_string().into(), + }; + assert_eq!( + selector + .on_request( + &crate::services::RouterRequest::fake_builder() + .build() + .unwrap() + ) + .unwrap(), + "test_static".into() + ); + assert_eq!(selector.on_drop().unwrap(), "test_static".into()); } #[test] @@ -1261,6 +1365,25 @@ mod test { .unwrap(), "test_static".into() ); + assert_eq!(selector.on_drop().unwrap(), "test_static".into()); + } + + #[test] + fn supergraph_static_field() { + let selector = SupergraphSelector::StaticField { + r#static: "test_static".to_string().into(), + }; + assert_eq!( + selector + .on_request( + &crate::services::SupergraphRequest::fake_builder() + .build() + .unwrap() + ) + .unwrap(), + "test_static".into() + ); + assert_eq!(selector.on_drop().unwrap(), "test_static".into()); } #[test] @@ -1321,6 +1444,29 @@ mod test { .unwrap(), "test_static".into() ); + assert_eq!(selector.on_drop().unwrap(), "test_static".into()); + } + + #[test] + fn subgraph_static_field() { + let selector = SubgraphSelector::StaticField { + r#static: "test_static".to_string().into(), + }; + assert_eq!( + selector + .on_request( + &crate::services::SubgraphRequest::fake_builder() + .supergraph_request(Arc::new( + http::Request::builder() + .body(crate::request::Request::builder().build()) + .unwrap() + )) + .build() + ) + .unwrap(), + "test_static".into() + ); + assert_eq!(selector.on_drop().unwrap(), "test_static".into()); } #[test] @@ -1819,6 +1965,27 @@ mod test { }); } + #[test] + fn test_router_studio_trace_id() { + let selector = RouterSelector::TraceId { + trace_id: TraceIdFormat::Apollo, + }; + let ctx = crate::Context::new(); + let _ = ctx.insert(APOLLO_OPERATION_ID, "42".to_string()).unwrap(); + + assert_eq!( + selector + .on_response( + &crate::services::RouterResponse::fake_builder() + .context(ctx) + .build() + .unwrap(), + ) + .unwrap(), + opentelemetry::Value::String("42".into()) + ); + } + #[test] fn router_env() { let selector = RouterSelector::Env { diff --git a/apollo-router/src/plugins/telemetry/config_new/spans.rs b/apollo-router/src/plugins/telemetry/config_new/spans.rs index 4d85f4b9e4..100875fb49 100644 --- a/apollo-router/src/plugins/telemetry/config_new/spans.rs +++ b/apollo-router/src/plugins/telemetry/config_new/spans.rs @@ -107,9 +107,11 @@ impl DefaultForLevel for SubgraphSpans { #[cfg(test)] mod test { + use std::str::FromStr; use std::sync::Arc; use http::header::USER_AGENT; + use jsonpath_rust::JsonPathInst; use opentelemetry_semantic_conventions::trace::GRAPHQL_DOCUMENT; use opentelemetry_semantic_conventions::trace::HTTP_REQUEST_METHOD; use opentelemetry_semantic_conventions::trace::NETWORK_PROTOCOL_VERSION; @@ -347,7 +349,7 @@ mod test { "test".to_string(), Conditional { selector: RouterSelector::StaticField { - r#static: "my-static-value".to_string(), + r#static: "my-static-value".to_string().into(), }, condition: Some(Arc::new(Mutex::new(Condition::Eq([ SelectorOrValue::Value(AttributeValue::Bool(true)), @@ -583,6 +585,41 @@ mod test { .any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))); } + #[test] + fn test_supergraph_response_event_custom_attribute() { + let mut spans = SupergraphSpans::default(); + spans.attributes.custom.insert( + "otel.status_code".to_string(), + Conditional { + selector: SupergraphSelector::StaticField { + r#static: String::from("error").into(), + }, + condition: Some(Arc::new(Mutex::new(Condition::Exists( + SupergraphSelector::ResponseErrors { + response_errors: JsonPathInst::from_str("$[0].extensions.code").unwrap(), + redact: None, + default: None, + }, + )))), + value: Arc::new(Default::default()), + }, + ); + let values = spans.attributes.on_response_event( + &graphql::Response::builder() + .error( + graphql::Error::builder() + .message("foo") + .extension_code("MY_EXTENSION_CODE") + .build(), + ) + .build(), + &Context::new(), + ); + assert!(values.iter().any(|key_val| key_val.key + == opentelemetry::Key::from_static_str("otel.status_code") + && key_val.value == opentelemetry::Value::String(String::from("error").into()))); + } + #[test] fn test_supergraph_response_custom_attribute() { let mut spans = SupergraphSpans::default(); diff --git a/apollo-router/src/plugins/telemetry/dynamic_attribute.rs b/apollo-router/src/plugins/telemetry/dynamic_attribute.rs index fb8b801167..1e2ea496af 100644 --- a/apollo-router/src/plugins/telemetry/dynamic_attribute.rs +++ b/apollo-router/src/plugins/telemetry/dynamic_attribute.rs @@ -6,6 +6,12 @@ use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::Layer; use tracing_subscriber::Registry; +use super::otel::layer::str_to_span_kind; +use super::otel::layer::str_to_status; +use super::otel::layer::SPAN_KIND_FIELD; +use super::otel::layer::SPAN_NAME_FIELD; +use super::otel::layer::SPAN_STATUS_CODE_FIELD; +use super::otel::layer::SPAN_STATUS_MESSAGE_FIELD; use super::otel::OtelData; use super::reload::IsSampled; use super::tracing::APOLLO_PRIVATE_PREFIX; @@ -73,6 +79,7 @@ impl SpanDynAttribute for ::tracing::Span { let mut extensions = s.extensions_mut(); match extensions.get_mut::() { Some(otel_data) => { + update_otel_data(otel_data, &key, &value); if otel_data.builder.attributes.is_none() { otel_data.builder.attributes = Some([(key, value)].into_iter().collect()); @@ -128,8 +135,23 @@ impl SpanDynAttribute for ::tracing::Span { match extensions.get_mut::() { Some(otel_data) => { if otel_data.builder.attributes.is_none() { - otel_data.builder.attributes = Some(attributes.collect()); + otel_data.builder.attributes = Some( + attributes + .inspect(|attr| { + update_otel_data( + otel_data, + &attr.key, + &attr.value, + ) + }) + .collect(), + ); } else { + let attributes: Vec = attributes + .inspect(|attr| { + update_otel_data(otel_data, &attr.key, &attr.value) + }) + .collect(); otel_data .builder .attributes @@ -170,6 +192,19 @@ impl SpanDynAttribute for ::tracing::Span { } } +fn update_otel_data(otel_data: &mut OtelData, key: &Key, value: &opentelemetry::Value) { + match key.as_str() { + SPAN_NAME_FIELD => otel_data.builder.name = format!("{:?}", value).into(), + SPAN_KIND_FIELD => otel_data.builder.span_kind = str_to_span_kind(&value.as_str()), + SPAN_STATUS_CODE_FIELD => otel_data.forced_status = str_to_status(&value.as_str()).into(), + SPAN_STATUS_MESSAGE_FIELD => { + otel_data.builder.status = + opentelemetry::trace::Status::error(value.as_str().to_string()) + } + _ => {} + } +} + /// To add dynamic attributes for spans pub(crate) trait EventDynAttribute { /// Always use before sending the event diff --git a/apollo-router/src/plugins/telemetry/metrics/apollo/studio.rs b/apollo-router/src/plugins/telemetry/metrics/apollo/studio.rs index 12f79bab2d..52dd2fbe22 100644 --- a/apollo-router/src/plugins/telemetry/metrics/apollo/studio.rs +++ b/apollo-router/src/plugins/telemetry/metrics/apollo/studio.rs @@ -194,6 +194,7 @@ impl From .collect(), query_latency_stats: Some(stats.query_latency_stats.into()), context: Some(stats.context), + extended_references: None, limits_stats: None, local_per_type_stat: HashMap::new(), operation_count: 0, diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index 4c2b884c01..61c92ed4c8 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -91,6 +91,7 @@ use crate::plugins::telemetry::apollo_exporter::proto::reports::StatsContext; use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config::MetricsCommon; use crate::plugins::telemetry::config::TracingCommon; +use crate::plugins::telemetry::config_new::graphql::GraphQLInstruments; use crate::plugins::telemetry::config_new::instruments::SupergraphInstruments; use crate::plugins::telemetry::dynamic_attribute::SpanDynAttribute; use crate::plugins::telemetry::fmt_layer::create_fmt_layer; @@ -578,13 +579,17 @@ impl Plugin for Telemetry { .instruments .new_supergraph_instruments(); custom_instruments.on_request(req); + let custom_graphql_instruments:GraphQLInstruments = (&config + .instrumentation + .instruments).into(); + custom_graphql_instruments.on_request(req); let supergraph_events = config.instrumentation.events.new_supergraph_events(); supergraph_events.on_request(req); - (req.context.clone(), custom_instruments, custom_attributes, supergraph_events) + (req.context.clone(), custom_instruments, custom_attributes, supergraph_events, custom_graphql_instruments) }, - move |(ctx, custom_instruments, custom_attributes, supergraph_events): (Context, SupergraphInstruments, Vec, SupergraphEvents), fut| { + move |(ctx, custom_instruments, custom_attributes, supergraph_events, custom_graphql_instruments): (Context, SupergraphInstruments, Vec, SupergraphEvents, GraphQLInstruments), fut| { let config = config_map_res.clone(); let sender = metrics_sender.clone(); let start = Instant::now(); @@ -598,11 +603,13 @@ impl Plugin for Telemetry { span.set_span_dyn_attributes(config.instrumentation.spans.supergraph.attributes.on_response(resp)); custom_instruments.on_response(resp); supergraph_events.on_response(resp); + custom_graphql_instruments.on_response(resp); }, Err(err) => { span.set_span_dyn_attributes(config.instrumentation.spans.supergraph.attributes.on_error(err)); custom_instruments.on_error(err, &ctx); supergraph_events.on_error(err, &ctx); + custom_graphql_instruments.on_error(err, &ctx); }, } result = Self::update_otel_metrics( @@ -612,6 +619,7 @@ impl Plugin for Telemetry { start.elapsed(), custom_instruments, supergraph_events, + custom_graphql_instruments, ) .await; Self::update_metrics_on_response_events( @@ -925,6 +933,7 @@ impl Telemetry { request_duration: Duration, custom_instruments: SupergraphInstruments, custom_events: SupergraphEvents, + custom_graphql_instruments: GraphQLInstruments, ) -> Result { let mut metric_attrs = { context @@ -951,9 +960,26 @@ impl Telemetry { let ctx = context.clone(); // Wait for the first response of the stream let (parts, stream) = response.response.into_parts(); + let config_cloned = config.clone(); let stream = stream.inspect(move |resp| { + let has_errors = !resp.errors.is_empty(); + // Useful for selector in spans/instruments/events + ctx.insert_json_value( + CONTAINS_GRAPHQL_ERROR, + serde_json_bytes::Value::Bool(has_errors), + ); + let span = Span::current(); + span.set_span_dyn_attributes( + config_cloned + .instrumentation + .spans + .supergraph + .attributes + .on_response_event(resp, &ctx), + ); custom_instruments.on_response_event(resp, &ctx); custom_events.on_response_event(resp, &ctx); + custom_graphql_instruments.on_response_event(resp, &ctx); }); let (first_response, rest) = stream.into_future().await; @@ -1269,11 +1295,6 @@ impl Telemetry { .enumerate() .map(move |(idx, response)| { let has_errors = !response.errors.is_empty(); - // Useful for selector in spans/instruments/events - ctx.insert_json_value( - CONTAINS_GRAPHQL_ERROR, - serde_json_bytes::Value::Bool(has_errors), - ); if !matches!(sender, Sender::Noop) { if operation_kind == OperationKind::Subscription { diff --git a/apollo-router/src/plugins/telemetry/otel/layer.rs b/apollo-router/src/plugins/telemetry/otel/layer.rs index 725a25d22b..269b1c41bb 100644 --- a/apollo-router/src/plugins/telemetry/otel/layer.rs +++ b/apollo-router/src/plugins/telemetry/otel/layer.rs @@ -29,10 +29,10 @@ use tracing_subscriber::Layer; use super::OtelData; use super::PreSampledTracer; -const SPAN_NAME_FIELD: &str = "otel.name"; -const SPAN_KIND_FIELD: &str = "otel.kind"; -const SPAN_STATUS_CODE_FIELD: &str = "otel.status_code"; -const SPAN_STATUS_MESSAGE_FIELD: &str = "otel.status_message"; +pub(crate) const SPAN_NAME_FIELD: &str = "otel.name"; +pub(crate) const SPAN_KIND_FIELD: &str = "otel.kind"; +pub(crate) const SPAN_STATUS_CODE_FIELD: &str = "otel.status_code"; +pub(crate) const SPAN_STATUS_MESSAGE_FIELD: &str = "otel.status_message"; const FIELD_EXCEPTION_MESSAGE: &str = "exception.message"; const FIELD_EXCEPTION_STACKTRACE: &str = "exception.stacktrace"; @@ -106,7 +106,7 @@ impl WithContext { } } -fn str_to_span_kind(s: &str) -> Option { +pub(crate) fn str_to_span_kind(s: &str) -> Option { match s { s if s.eq_ignore_ascii_case("server") => Some(otel::SpanKind::Server), s if s.eq_ignore_ascii_case("client") => Some(otel::SpanKind::Client), @@ -117,7 +117,7 @@ fn str_to_span_kind(s: &str) -> Option { } } -fn str_to_status(s: &str) -> otel::Status { +pub(crate) fn str_to_status(s: &str) -> otel::Status { match s { s if s.eq_ignore_ascii_case("ok") => otel::Status::Ok, s if s.eq_ignore_ascii_case("error") => otel::Status::error(""), @@ -331,7 +331,9 @@ impl<'a> field::Visit for SpanAttributeVisitor<'a> { match field.name() { SPAN_NAME_FIELD => self.span_builder.name = value.to_string().into(), SPAN_KIND_FIELD => self.span_builder.span_kind = str_to_span_kind(value), - SPAN_STATUS_CODE_FIELD => self.span_builder.status = str_to_status(value), + SPAN_STATUS_CODE_FIELD => { + self.span_builder.status = str_to_status(value); + } SPAN_STATUS_MESSAGE_FIELD => { self.span_builder.status = otel::Status::error(value.to_string()) } @@ -706,6 +708,7 @@ where builder, parent_cx, event_attributes: None, + forced_status: None, }); } @@ -880,6 +883,7 @@ where if let Some(OtelData { mut builder, parent_cx, + forced_status, .. }) = extensions.remove::() { @@ -896,6 +900,9 @@ where attributes.insert(idle_ns, timings.idle.into()); } } + if let Some(forced_status) = forced_status { + builder.status = forced_status; + } // Assign end time, build and start span, drop span to export builder @@ -985,6 +992,7 @@ mod tests { builder, parent_cx: parent_cx.clone(), event_attributes: None, + forced_status: None, }); noop::NoopSpan::new() } diff --git a/apollo-router/src/plugins/telemetry/otel/mod.rs b/apollo-router/src/plugins/telemetry/otel/mod.rs index 81c378bdbc..c6173a5fe5 100644 --- a/apollo-router/src/plugins/telemetry/otel/mod.rs +++ b/apollo-router/src/plugins/telemetry/otel/mod.rs @@ -26,4 +26,7 @@ pub(crate) struct OtelData { /// Attributes gathered for the next event pub(crate) event_attributes: Option>, + + /// Forced status in case it's coming from the custom attributes + pub(crate) forced_status: Option, } diff --git a/apollo-router/src/plugins/telemetry/otel/tracer.rs b/apollo-router/src/plugins/telemetry/otel/tracer.rs index 4a369568d3..ee9dedb33f 100644 --- a/apollo-router/src/plugins/telemetry/otel/tracer.rs +++ b/apollo-router/src/plugins/telemetry/otel/tracer.rs @@ -187,6 +187,7 @@ mod tests { builder, parent_cx, event_attributes: None, + forced_status: None, }); let span = cx.span(); let span_context = span.span_context(); @@ -230,6 +231,7 @@ mod tests { builder, parent_cx, event_attributes: None, + forced_status: None, }); assert_eq!( diff --git a/apollo-router/src/plugins/telemetry/proto/reports.proto b/apollo-router/src/plugins/telemetry/proto/reports.proto index 0f03a7900e..2d9062f8d8 100644 --- a/apollo-router/src/plugins/telemetry/proto/reports.proto +++ b/apollo-router/src/plugins/telemetry/proto/reports.proto @@ -546,6 +546,11 @@ message Report { // operations. If this is false, each operation is described in precisely // one of those two fields. bool traces_pre_aggregated = 7; + + // This indicates whether or not extended references are enabled, which are within the stats with context and contain + // input type and enum value references. We need this flag so we can tell if the option is enabled even when there are + // no extended references to report. + bool extended_references_enabled = 9; } @@ -556,6 +561,9 @@ message ContextualizedStats { // field executions and thus only reflects operations for which field-level tracing occurred. map per_type_stat = 3; + // Extended references including input types and enum values. + ExtendedReferences extended_references = 6; + // Per type stats that are obtained directly by the router or gateway rather than FTV1. map local_per_type_stat = 7; @@ -578,6 +586,34 @@ message QueryMetadata { string pq_id = 3; } +message ExtendedReferences { + map input_types = 1; + + // Map of enum name to stats about that enum. + map enum_values = 2; +} + +message InputTypeStats { + // Map of input object type to the stats about the fields within that object. + map field_names = 1; +} + +message InputFieldStats { + // The total number of operations that reference the input object field. + uint64 refs = 1; + + // The number of operations that reference the input object field as a null value. + uint64 null_refs = 2; + + // The number of operations that don't reference this input object field (the field is missing or undefined). + uint64 missing = 3; +} + +message EnumStats { + // Map of enum value name to the number of referencing operations. + map enum_values = 1; +} + // A sequence of traces and stats. If Report.traces_pre_aggregated (at the top // level of the report) is false, an individual operation should either be // described as a trace or as part of stats, but not both. If that flag diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 00af3889da..906f70fa62 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -541,7 +541,10 @@ impl BridgeQueryPlanner { plan_success .data .query_plan - .hash_subqueries(&self.subgraph_schemas, &self.schema.raw_sdl)?; + .init_parsed_operations_and_hash_subqueries( + &self.subgraph_schemas, + &self.schema.raw_sdl, + )?; plan_success .data .query_plan @@ -969,13 +972,16 @@ pub(super) struct QueryPlan { } impl QueryPlan { - fn hash_subqueries( + fn init_parsed_operations_and_hash_subqueries( &mut self, subgraph_schemas: &SubgraphSchemas, supergraph_schema_hash: &str, ) -> Result<(), ValidationErrors> { if let Some(node) = self.node.as_mut() { - node.hash_subqueries(subgraph_schemas, supergraph_schema_hash)?; + node.init_parsed_operations_and_hash_subqueries( + subgraph_schemas, + supergraph_schema_hash, + )?; } Ok(()) } @@ -1332,9 +1338,18 @@ mod tests { #[test(tokio::test)] async fn test_subselections() { + let mut configuration: Configuration = Default::default(); + configuration.supergraph.introspection = true; + let configuration = Arc::new(configuration); + + let planner = + BridgeQueryPlanner::new(EXAMPLE_SCHEMA.to_string(), configuration.clone(), None) + .await + .unwrap(); + macro_rules! s { ($query: expr) => { - insta::assert_snapshot!(subselections_keys($query).await); + insta::assert_snapshot!(subselections_keys($query, &planner).await); }; } s!("query Q { me { username name { first last }}}"); @@ -1472,7 +1487,7 @@ mod tests { }}"#); } - async fn subselections_keys(query: &str) -> String { + async fn subselections_keys(query: &str, planner: &BridgeQueryPlanner) -> String { fn check_query_plan_coverage( node: &PlanNode, parent_label: Option<&str>, @@ -1585,9 +1600,26 @@ mod tests { } } - let result = plan(EXAMPLE_SCHEMA, query, query, None, PlanOptions::default()) + let mut configuration: Configuration = Default::default(); + configuration.supergraph.introspection = true; + let configuration = Arc::new(configuration); + + let doc = Query::parse_document(query, None, &planner.schema(), &configuration).unwrap(); + + let result = planner + .get( + QueryKey { + original_query: query.to_string(), + filtered_query: query.to_string(), + operation_name: None, + metadata: CacheKeyMetadata::default(), + plan_options: PlanOptions::default(), + }, + doc, + ) .await .unwrap(); + if let QueryPlannerContent::Plan { plan, .. } = result { check_query_plan_coverage(&plan.root, None, &plan.query.subselections); diff --git a/apollo-router/src/query_planner/bridge_query_planner_pool.rs b/apollo-router/src/query_planner/bridge_query_planner_pool.rs index b67d8ce2ee..67d7a4c3e3 100644 --- a/apollo-router/src/query_planner/bridge_query_planner_pool.rs +++ b/apollo-router/src/query_planner/bridge_query_planner_pool.rs @@ -197,8 +197,7 @@ impl tower::Service for BridgeQueryPlannerPool { f64_histogram!( "apollo.router.query_planning.total.duration", "Duration of the time the router waited for a query plan, including both the queue time and planning time.", - start.elapsed().as_secs_f64(), - [] + start.elapsed().as_secs_f64() ); res diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index a6b7594ab2..f6b04aba23 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -32,6 +32,7 @@ use crate::plugins::authorization::AuthorizationPlugin; use crate::plugins::authorization::CacheKeyMetadata; use crate::plugins::progressive_override::LABELS_TO_OVERRIDE_KEY; use crate::plugins::telemetry::utils::Timer; +use crate::query_planner::fetch::SubgraphSchemas; use crate::query_planner::labeler::add_defer_labels; use crate::query_planner::BridgeQueryPlannerPool; use crate::query_planner::QueryPlanResult; @@ -51,6 +52,7 @@ use crate::Context; pub(crate) type Plugins = IndexMap>; pub(crate) type InMemoryCachePlanner = InMemoryCache>>; +pub(crate) const APOLLO_OPERATION_ID: &str = "apollo_operation_id"; #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize)] pub(crate) enum ConfigMode { @@ -71,12 +73,26 @@ pub(crate) struct CachingQueryPlanner { >, delegate: T, schema: Arc, + subgraph_schemas: Arc>>>, plugins: Arc, enable_authorization_directives: bool, config_mode: ConfigMode, introspection: bool, } +fn init_query_plan_from_redis( + subgraph_schemas: &SubgraphSchemas, + cache_entry: &mut Result>, +) -> Result<(), String> { + if let Ok(QueryPlannerContent::Plan { plan }) = cache_entry { + Arc::make_mut(plan) + .root + .init_parsed_operations(subgraph_schemas) + .map_err(|e| format!("Invalid subgraph operation: {e}"))? + } + Ok(()) +} + impl CachingQueryPlanner where T: tower::Service< @@ -90,6 +106,7 @@ where pub(crate) async fn new( delegate: T, schema: Arc, + subgraph_schemas: Arc>>>, configuration: &Configuration, plugins: Plugins, ) -> Result, BoxError> { @@ -119,6 +136,7 @@ where cache, delegate, schema, + subgraph_schemas, plugins: Arc::new(plugins), enable_authorization_directives, config_mode, @@ -264,7 +282,12 @@ where } } - let entry = self.cache.get(&caching_key).await; + let entry = self + .cache + .get(&caching_key, |v| { + init_query_plan_from_redis(&self.subgraph_schemas, v) + }) + .await; if entry.is_first() { let doc = match query_analysis.parse_document(&query, operation.as_deref()) { Ok(doc) => doc, @@ -362,7 +385,7 @@ where urp.cloned() } { let _ = response.context.insert( - "apollo_operation_id", + APOLLO_OPERATION_ID, stats_report_key_hash(usage_reporting.stats_report_key.as_str()), ); let _ = response.context.insert( @@ -433,7 +456,12 @@ where }; let context = request.context.clone(); - let entry = self.cache.get(&caching_key).await; + let entry = self + .cache + .get(&caching_key, |v| { + init_query_plan_from_redis(&self.subgraph_schemas, v) + }) + .await; if entry.is_first() { let query_planner::CachingRequest { mut query, @@ -696,10 +724,15 @@ mod tests { let schema = include_str!("testdata/schema.graphql"); let schema = Arc::new(Schema::parse_test(schema, &configuration).unwrap()); - let mut planner = - CachingQueryPlanner::new(delegate, schema.clone(), &configuration, IndexMap::new()) - .await - .unwrap(); + let mut planner = CachingQueryPlanner::new( + delegate, + schema.clone(), + Default::default(), + &configuration, + IndexMap::new(), + ) + .await + .unwrap(); let configuration = Configuration::default(); @@ -792,10 +825,15 @@ mod tests { ) .unwrap(); - let mut planner = - CachingQueryPlanner::new(delegate, Arc::new(schema), &configuration, IndexMap::new()) - .await - .unwrap(); + let mut planner = CachingQueryPlanner::new( + delegate, + Arc::new(schema), + Default::default(), + &configuration, + IndexMap::new(), + ) + .await + .unwrap(); let context = Context::new(); context.extensions().lock().insert::(doc); diff --git a/apollo-router/src/query_planner/convert.rs b/apollo-router/src/query_planner/convert.rs index 2f24e35b9d..e75a2474b9 100644 --- a/apollo-router/src/query_planner/convert.rs +++ b/apollo-router/src/query_planner/convert.rs @@ -74,6 +74,7 @@ impl From<&'_ Box> for plan::PlanNode { operation_kind, input_rewrites, output_rewrites, + context_rewrites, } = &**value; Self::Fetch(super::fetch::FetchNode { service_name: subgraph_name.clone(), @@ -86,6 +87,7 @@ impl From<&'_ Box> for plan::PlanNode { id: id.map(|id| id.to_string().into()), input_rewrites: option_vec(input_rewrites), output_rewrites: option_vec(output_rewrites), + context_rewrites: option_vec(context_rewrites), schema_aware_hash: Default::default(), authorization: Default::default(), }) @@ -153,6 +155,7 @@ impl From<&'_ next::FetchNode> for subscription::SubscriptionNode { operation_kind, input_rewrites, output_rewrites, + context_rewrites: _, } = value; Self { service_name: subgraph_name.clone(), diff --git a/apollo-router/src/query_planner/execution.rs b/apollo-router/src/query_planner/execution.rs index 801069afd1..dc1e27123e 100644 --- a/apollo-router/src/query_planner/execution.rs +++ b/apollo-router/src/query_planner/execution.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; +use apollo_compiler::validation::Valid; use apollo_compiler::NodeStr; use futures::future::join_all; use futures::prelude::*; @@ -50,6 +51,7 @@ impl QueryPlan { service_factory: &'a Arc, supergraph_request: &'a Arc>, schema: &'a Arc, + subgraph_schemas: &'a Arc>>>, sender: mpsc::Sender, subscription_handle: Option, subscription_config: &'a Option, @@ -73,6 +75,7 @@ impl QueryPlan { root_node: &self.root, subscription_handle: &subscription_handle, subscription_config, + subgraph_schemas, }, &root, &initial_value.unwrap_or_default(), @@ -100,6 +103,7 @@ pub(crate) struct ExecutionParameters<'a> { pub(crate) context: &'a Context, pub(crate) service_factory: &'a Arc, pub(crate) schema: &'a Arc, + pub(crate) subgraph_schemas: &'a Arc>>>, pub(crate) supergraph_request: &'a Arc>, pub(crate) deferred_fetches: &'a HashMap)>>, pub(crate) query: &'a Arc, @@ -293,6 +297,7 @@ impl PlanNode { root_node: parameters.root_node, subscription_handle: parameters.subscription_handle, subscription_config: parameters.subscription_config, + subgraph_schemas: parameters.subgraph_schemas, }, current_dir, &value, @@ -435,6 +440,7 @@ impl DeferredNode { let label = self.label.as_ref().map(|l| l.to_string()); let tx = sender; let sc = parameters.schema.clone(); + let subgraph_schemas = parameters.subgraph_schemas.clone(); let orig = parameters.supergraph_request.clone(); let sf = parameters.service_factory.clone(); let root_node = parameters.root_node.clone(); @@ -481,6 +487,7 @@ impl DeferredNode { root_node: &root_node, subscription_handle: &subscription_handle, subscription_config: &subscription_config, + subgraph_schemas: &subgraph_schemas, }, &Path::default(), &value, diff --git a/apollo-router/src/query_planner/fetch.rs b/apollo-router/src/query_planner/fetch.rs index f76a6a2d2d..1bc1d75181 100644 --- a/apollo-router/src/query_planner/fetch.rs +++ b/apollo-router/src/query_planner/fetch.rs @@ -2,11 +2,11 @@ use std::collections::HashMap; use std::fmt::Display; use std::sync::Arc; +use apollo_compiler::ast; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::NodeStr; use indexmap::IndexSet; -use once_cell::sync::OnceCell as OnceLock; use serde::Deserialize; use serde::Serialize; use tower::ServiceExt; @@ -17,6 +17,9 @@ use super::execution::ExecutionParameters; use super::rewrites; use super::selection::execute_selection_set; use super::selection::Selection; +use super::subgraph_context::build_operation_with_aliasing; +use super::subgraph_context::ContextualArguments; +use super::subgraph_context::SubgraphContext; use crate::error::Error; use crate::error::FetchError; use crate::error::ValidationErrors; @@ -38,6 +41,7 @@ use crate::spec::Schema; #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[non_exhaustive] +#[cfg_attr(test, derive(schemars::JsonSchema))] pub enum OperationKind { #[default] Query, @@ -70,22 +74,22 @@ impl OperationKind { } } -impl From for apollo_compiler::ast::OperationType { +impl From for ast::OperationType { fn from(value: OperationKind) -> Self { match value { - OperationKind::Query => apollo_compiler::ast::OperationType::Query, - OperationKind::Mutation => apollo_compiler::ast::OperationType::Mutation, - OperationKind::Subscription => apollo_compiler::ast::OperationType::Subscription, + OperationKind::Query => ast::OperationType::Query, + OperationKind::Mutation => ast::OperationType::Mutation, + OperationKind::Subscription => ast::OperationType::Subscription, } } } -impl From for OperationKind { - fn from(value: apollo_compiler::ast::OperationType) -> Self { +impl From for OperationKind { + fn from(value: ast::OperationType) -> Self { match value { - apollo_compiler::ast::OperationType::Query => OperationKind::Query, - apollo_compiler::ast::OperationType::Mutation => OperationKind::Mutation, - apollo_compiler::ast::OperationType::Subscription => OperationKind::Subscription, + ast::OperationType::Query => OperationKind::Query, + ast::OperationType::Mutation => OperationKind::Mutation, + ast::OperationType::Subscription => OperationKind::Subscription, } } } @@ -125,6 +129,9 @@ pub(crate) struct FetchNode { // Optionally describes a number of "rewrites" to apply to the data that received from a fetch (and before it is applied to the current in-memory results). pub(crate) output_rewrites: Option>, + // Optionally describes a number of "rewrites" to apply to the data that has already been received further up the tree + pub(crate) context_rewrites: Option>, + // hash for the query and relevant parts of the schema. if two different schemas provide the exact same types, fields and directives // affecting the query, then they will have the same hash #[serde(default)] @@ -137,53 +144,70 @@ pub(crate) struct FetchNode { #[derive(Clone)] pub(crate) struct SubgraphOperation { - // At least one of these two must be initialized - serialized: OnceLock, - parsed: OnceLock>>, + serialized: String, + /// Ideally this would be always present, but we don’t have access to the subgraph schemas + /// during `Deserialize`. + parsed: Option>>, } impl SubgraphOperation { pub(crate) fn from_string(serialized: impl Into) -> Self { Self { - serialized: OnceLock::from(serialized.into()), - parsed: OnceLock::new(), + serialized: serialized.into(), + parsed: None, } } pub(crate) fn from_parsed(parsed: impl Into>>) -> Self { + let parsed = parsed.into(); Self { - serialized: OnceLock::new(), - parsed: OnceLock::from(parsed.into()), + serialized: parsed.to_string(), + parsed: Some(parsed), } } pub(crate) fn as_serialized(&self) -> &str { - self.serialized.get_or_init(|| { - self.parsed - .get() - .expect("SubgraphOperation has neither representation initialized") - .to_string() - }) + &self.serialized } - pub(crate) fn as_parsed( - &self, + pub(crate) fn init_parsed( + &mut self, subgraph_schema: &Valid, ) -> Result<&Arc>, ValidationErrors> { - self.parsed.get_or_try_init(|| { - let serialized = self - .serialized - .get() - .expect("SubgraphOperation has neither representation initialized"); - Ok(Arc::new( - ExecutableDocument::parse_and_validate( + match &mut self.parsed { + Some(parsed) => Ok(parsed), + option => { + let parsed = Arc::new(ExecutableDocument::parse_and_validate( subgraph_schema, - serialized, + &self.serialized, "operation.graphql", - ) - .map_err(|e| e.errors)?, - )) - }) + )?); + Ok(option.insert(parsed)) + } + } + } + + pub(crate) fn as_parsed( + &self, + ) -> Result<&Arc>, SubgraphOperationNotInitialized> { + self.parsed.as_ref().ok_or(SubgraphOperationNotInitialized) + } +} + +/// Failed to call `SubgraphOperation::init_parsed` after creating a query plan +#[derive(Debug, displaydoc::Display, thiserror::Error)] +pub(crate) struct SubgraphOperationNotInitialized; + +impl SubgraphOperationNotInitialized { + pub(crate) fn into_graphql_errors(self) -> Vec { + vec![graphql::Error::builder() + .extension_code(self.code()) + .message(self.to_string()) + .build()] + } + + pub(crate) fn code(&self) -> &'static str { + "SUBGRAPH_OPERATION_NOT_INITIALIZED" } } @@ -243,6 +267,7 @@ impl Display for QueryHash { pub(crate) struct Variables { pub(crate) variables: Object, pub(crate) inverted_paths: Vec>, + pub(crate) contextual_arguments: Option, } impl Variables { @@ -256,8 +281,10 @@ impl Variables { request: &Arc>, schema: &Schema, input_rewrites: &Option>, + context_rewrites: &Option>, ) -> Option { let body = request.body(); + let mut subgraph_context = SubgraphContext::new(data, schema, context_rewrites); if !requires.is_empty() { let mut variables = Object::with_capacity(1 + variable_usages.len()); @@ -269,8 +296,12 @@ impl Variables { let mut inverted_paths: Vec> = Vec::new(); let mut values: IndexSet = IndexSet::new(); - data.select_values_and_paths(schema, current_dir, |path, value| { + // first get contextual values that are required + if let Some(context) = subgraph_context.as_mut() { + context.execute_on_path(path); + } + let mut value = execute_selection_set(value, requires, schema, None); if value.as_object().map(|o| !o.is_empty()).unwrap_or(false) { rewrites::apply_rewrites(schema, &mut value, input_rewrites); @@ -292,11 +323,16 @@ impl Variables { } let representations = Value::Array(Vec::from_iter(values)); + let contextual_arguments = match subgraph_context.as_mut() { + Some(context) => context.add_variables_and_get_args(&mut variables), + None => None, + }; variables.insert("representations", representations); Some(Variables { variables, inverted_paths, + contextual_arguments, }) } else { // with nested operations (Query or Mutation has an operation returning a Query or Mutation), @@ -323,20 +359,13 @@ impl Variables { }) .collect::(), inverted_paths: Vec::new(), + contextual_arguments: None, }) } } } impl FetchNode { - pub(crate) fn parsed_operation( - &self, - subgraph_schemas: &SubgraphSchemas, - ) -> Result<&Arc>, ValidationErrors> { - self.operation - .as_parsed(&subgraph_schemas[self.service_name.as_str()]) - } - #[allow(clippy::too_many_arguments)] pub(crate) async fn fetch_node<'a>( &'a self, @@ -355,6 +384,7 @@ impl FetchNode { let Variables { variables, inverted_paths: paths, + contextual_arguments, } = match Variables::new( &self.requires, &self.variable_usages, @@ -364,6 +394,7 @@ impl FetchNode { parameters.supergraph_request, parameters.schema, &self.input_rewrites, + &self.context_rewrites, ) { Some(variables) => variables, None => { @@ -371,6 +402,35 @@ impl FetchNode { } }; + let alias_query_string; // this exists outside the if block to allow the as_str() to be longer lived + let aliased_operation = if let Some(ctx_arg) = contextual_arguments { + if let Some(subgraph_schema) = + parameters.subgraph_schemas.get(&service_name.to_string()) + { + match build_operation_with_aliasing(operation, &ctx_arg, subgraph_schema) { + Ok(op) => { + alias_query_string = op.serialize().no_indent().to_string(); + alias_query_string.as_str() + } + Err(errors) => { + tracing::debug!( + "couldn't generate a valid executable document? {:?}", + errors + ); + operation.as_serialized() + } + } + } else { + tracing::debug!( + "couldn't find a subgraph schema for service {:?}", + &service_name + ); + operation.as_serialized() + } + } else { + operation.as_serialized() + }; + let mut subgraph_request = SubgraphRequest::builder() .supergraph_request(parameters.supergraph_request.clone()) .subgraph_request( @@ -389,7 +449,7 @@ impl FetchNode { ) .body( Request::builder() - .query(operation.as_serialized()) + .query(aliased_operation) .and_operation_name(operation_name.as_ref().map(|n| n.to_string())) .variables(variables.clone()) .build(), @@ -607,13 +667,22 @@ impl FetchNode { &self.operation_kind } - pub(crate) fn hash_subquery( + pub(crate) fn init_parsed_operation( + &mut self, + subgraph_schemas: &SubgraphSchemas, + ) -> Result<(), ValidationErrors> { + let schema = &subgraph_schemas[self.service_name.as_str()]; + self.operation.init_parsed(schema)?; + Ok(()) + } + + pub(crate) fn init_parsed_operation_and_hash_subquery( &mut self, subgraph_schemas: &SubgraphSchemas, supergraph_schema_hash: &str, ) -> Result<(), ValidationErrors> { - let doc = self.parsed_operation(subgraph_schemas)?; let schema = &subgraph_schemas[self.service_name.as_str()]; + let doc = self.operation.init_parsed(schema)?; if let Ok(hash) = QueryHashVisitor::hash_query( schema, diff --git a/apollo-router/src/query_planner/mod.rs b/apollo-router/src/query_planner/mod.rs index a11a5cb072..58285e3638 100644 --- a/apollo-router/src/query_planner/mod.rs +++ b/apollo-router/src/query_planner/mod.rs @@ -20,6 +20,7 @@ mod labeler; mod plan; pub(crate) mod rewrites; mod selection; +mod subgraph_context; pub(crate) mod subscription; pub(crate) const FETCH_SPAN_NAME: &str = "fetch"; diff --git a/apollo-router/src/query_planner/plan.rs b/apollo-router/src/query_planner/plan.rs index 5acd3ec8b4..a6421990d8 100644 --- a/apollo-router/src/query_planner/plan.rs +++ b/apollo-router/src/query_planner/plan.rs @@ -34,7 +34,7 @@ pub(crate) struct QueryKey { } /// A plan for a given GraphQL query -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct QueryPlan { pub(crate) usage_reporting: Arc, pub(crate) root: PlanNode, @@ -320,44 +320,112 @@ impl PlanNode { } } - pub(crate) fn hash_subqueries( + pub(crate) fn init_parsed_operations( + &mut self, + subgraph_schemas: &SubgraphSchemas, + ) -> Result<(), ValidationErrors> { + match self { + PlanNode::Fetch(fetch_node) => { + fetch_node.init_parsed_operation(subgraph_schemas)?; + } + + PlanNode::Sequence { nodes } => { + for node in nodes { + node.init_parsed_operations(subgraph_schemas)?; + } + } + PlanNode::Parallel { nodes } => { + for node in nodes { + node.init_parsed_operations(subgraph_schemas)?; + } + } + PlanNode::Flatten(flatten) => flatten.node.init_parsed_operations(subgraph_schemas)?, + PlanNode::Defer { primary, deferred } => { + if let Some(node) = primary.node.as_mut() { + node.init_parsed_operations(subgraph_schemas)?; + } + for deferred_node in deferred { + if let Some(node) = &mut deferred_node.node { + Arc::make_mut(node).init_parsed_operations(subgraph_schemas)?; + } + } + } + PlanNode::Subscription { primary: _, rest } => { + if let Some(node) = rest.as_mut() { + node.init_parsed_operations(subgraph_schemas)?; + } + } + PlanNode::Condition { + condition: _, + if_clause, + else_clause, + } => { + if let Some(node) = if_clause.as_mut() { + node.init_parsed_operations(subgraph_schemas)?; + } + if let Some(node) = else_clause.as_mut() { + node.init_parsed_operations(subgraph_schemas)?; + } + } + } + Ok(()) + } + + pub(crate) fn init_parsed_operations_and_hash_subqueries( &mut self, subgraph_schemas: &SubgraphSchemas, supergraph_schema_hash: &str, ) -> Result<(), ValidationErrors> { match self { PlanNode::Fetch(fetch_node) => { - fetch_node.hash_subquery(subgraph_schemas, supergraph_schema_hash)?; + fetch_node.init_parsed_operation_and_hash_subquery( + subgraph_schemas, + supergraph_schema_hash, + )?; } PlanNode::Sequence { nodes } => { for node in nodes { - node.hash_subqueries(subgraph_schemas, supergraph_schema_hash)?; + node.init_parsed_operations_and_hash_subqueries( + subgraph_schemas, + supergraph_schema_hash, + )?; } } PlanNode::Parallel { nodes } => { for node in nodes { - node.hash_subqueries(subgraph_schemas, supergraph_schema_hash)?; + node.init_parsed_operations_and_hash_subqueries( + subgraph_schemas, + supergraph_schema_hash, + )?; } } - PlanNode::Flatten(flatten) => flatten - .node - .hash_subqueries(subgraph_schemas, supergraph_schema_hash)?, + PlanNode::Flatten(flatten) => flatten.node.init_parsed_operations_and_hash_subqueries( + subgraph_schemas, + supergraph_schema_hash, + )?, PlanNode::Defer { primary, deferred } => { if let Some(node) = primary.node.as_mut() { - node.hash_subqueries(subgraph_schemas, supergraph_schema_hash)?; + node.init_parsed_operations_and_hash_subqueries( + subgraph_schemas, + supergraph_schema_hash, + )?; } for deferred_node in deferred { - if let Some(node) = deferred_node.node.take() { - let mut new_node = (*node).clone(); - new_node.hash_subqueries(subgraph_schemas, supergraph_schema_hash)?; - deferred_node.node = Some(Arc::new(new_node)); + if let Some(node) = &mut deferred_node.node { + Arc::make_mut(node).init_parsed_operations_and_hash_subqueries( + subgraph_schemas, + supergraph_schema_hash, + )? } } } PlanNode::Subscription { primary: _, rest } => { if let Some(node) = rest.as_mut() { - node.hash_subqueries(subgraph_schemas, supergraph_schema_hash)?; + node.init_parsed_operations_and_hash_subqueries( + subgraph_schemas, + supergraph_schema_hash, + )?; } } PlanNode::Condition { @@ -366,10 +434,16 @@ impl PlanNode { else_clause, } => { if let Some(node) = if_clause.as_mut() { - node.hash_subqueries(subgraph_schemas, supergraph_schema_hash)?; + node.init_parsed_operations_and_hash_subqueries( + subgraph_schemas, + supergraph_schema_hash, + )?; } if let Some(node) = else_clause.as_mut() { - node.hash_subqueries(subgraph_schemas, supergraph_schema_hash)?; + node.init_parsed_operations_and_hash_subqueries( + subgraph_schemas, + supergraph_schema_hash, + )?; } } } diff --git a/apollo-router/src/query_planner/rewrites.rs b/apollo-router/src/query_planner/rewrites.rs index 94453630df..f3b10a7fa4 100644 --- a/apollo-router/src/query_planner/rewrites.rs +++ b/apollo-router/src/query_planner/rewrites.rs @@ -47,7 +47,7 @@ pub(crate) struct DataKeyRenamer { } impl DataRewrite { - fn maybe_apply(&self, schema: &Schema, data: &mut Value) { + pub(crate) fn maybe_apply(&self, schema: &Schema, data: &mut Value) { match self { DataRewrite::ValueSetter(setter) => { // The `path` of rewrites can only be either `Key` or `Fragment`, and so far diff --git a/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__bridge_query_planner__tests__plan_root.snap b/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__bridge_query_planner__tests__plan_root.snap index 974ccc03c5..d49c351866 100644 --- a/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__bridge_query_planner__tests__plan_root.snap +++ b/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__bridge_query_planner__tests__plan_root.snap @@ -13,6 +13,7 @@ Fetch( id: None, input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: QueryHash( "a4ab3ffe0fd7863aea8cd1e85d019d2c64ec0351d62f9759bed3c9dc707ea315", ), diff --git a/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__tests__query_plan_from_json.snap b/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__tests__query_plan_from_json.snap index ef8a64f2a6..c18018d7a2 100644 --- a/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__tests__query_plan_from_json.snap +++ b/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__tests__query_plan_from_json.snap @@ -17,6 +17,7 @@ Sequence { id: None, input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: QueryHash( "", ), @@ -81,6 +82,7 @@ Sequence { id: None, input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: QueryHash( "", ), @@ -155,6 +157,7 @@ Sequence { id: None, input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: QueryHash( "", ), @@ -216,6 +219,7 @@ Sequence { id: None, input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: QueryHash( "", ), @@ -287,6 +291,7 @@ Sequence { id: None, input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: QueryHash( "", ), diff --git a/apollo-router/src/query_planner/subgraph_context.rs b/apollo-router/src/query_planner/subgraph_context.rs new file mode 100644 index 0000000000..9e87f38703 --- /dev/null +++ b/apollo-router/src/query_planner/subgraph_context.rs @@ -0,0 +1,460 @@ +use std::collections::HashMap; +use std::collections::HashSet; + +use apollo_compiler::ast; +use apollo_compiler::ast::Name; +use apollo_compiler::ast::VariableDefinition; +use apollo_compiler::executable; +use apollo_compiler::executable::Operation; +use apollo_compiler::executable::Selection; +use apollo_compiler::executable::SelectionSet; +use apollo_compiler::validation::Valid; +use apollo_compiler::validation::WithErrors; +use apollo_compiler::ExecutableDocument; +use apollo_compiler::Node; +use serde_json_bytes::ByteString; +use serde_json_bytes::Map; + +use super::fetch::SubgraphOperation; +use super::rewrites::DataKeyRenamer; +use super::rewrites::DataRewrite; +use crate::json_ext::Path; +use crate::json_ext::PathElement; +use crate::json_ext::Value; +use crate::json_ext::ValueExt; +use crate::spec::Schema; + +#[derive(Debug)] +pub(crate) struct ContextualArguments { + pub(crate) arguments: HashSet, // a set of all argument names that will be passed to the subgraph. This is the unmodified name from the query plan + pub(crate) count: usize, // the number of different sets of arguments that exist. This will either be 1 or the number of entities +} + +pub(crate) struct SubgraphContext<'a> { + pub(crate) data: &'a Value, + pub(crate) schema: &'a Schema, + pub(crate) context_rewrites: &'a Vec, + pub(crate) named_args: Vec>, +} + +// context_path is a non-standard relative path which may navigate up the tree +// from the current position. This is indicated with a ".." PathElement::Key +// note that the return value is an absolute path that may be used anywhere +fn merge_context_path( + current_dir: &Path, + context_path: &Path, +) -> Result { + let mut i = 0; + let mut j = current_dir.len(); + // iterate over the context_path(i), every time we encounter a '..', we want + // to go up one level in the current_dir(j) + while i < context_path.len() { + match &context_path.0.get(i) { + Some(PathElement::Key(e, _)) => { + let mut found = false; + if e == ".." { + while !found { + if j == 0 { + return Err(ContextBatchingError::InvalidRelativePath); + } + j -= 1; + + if let Some(PathElement::Key(_, _)) = current_dir.0.get(j) { + found = true; + } + } + i += 1; + } else { + break; + } + } + _ => break, + } + } + + let mut return_path: Vec = current_dir.iter().take(j).cloned().collect(); + + context_path.iter().skip(i).for_each(|e| { + return_path.push(e.clone()); + }); + Ok(Path(return_path.into_iter().collect())) +} + +impl<'a> SubgraphContext<'a> { + pub(crate) fn new( + data: &'a Value, + schema: &'a Schema, + context_rewrites: &'a Option>, + ) -> Option> { + if let Some(rewrites) = context_rewrites { + if !rewrites.is_empty() { + return Some(SubgraphContext { + data, + schema, + context_rewrites: rewrites, + named_args: Vec::new(), + }); + } + } + None + } + + // For each of the rewrites, start collecting data for the data at path. + // Once we find a Value for a given variable, skip additional rewrites that + // reference the same variable + pub(crate) fn execute_on_path(&mut self, path: &Path) { + let mut found_rewrites: HashSet = HashSet::new(); + let hash_map: HashMap = self + .context_rewrites + .iter() + .filter_map(|rewrite| { + match rewrite { + DataRewrite::KeyRenamer(item) => { + if !found_rewrites.contains(item.rename_key_to.as_str()) { + let wrapped_data_path = merge_context_path(path, &item.path); + if let Ok(data_path) = wrapped_data_path { + let val = self.data.get_path(self.schema, &data_path); + + if let Ok(v) = val { + // add to found + found_rewrites.insert(item.rename_key_to.clone().to_string()); + // TODO: not great + let mut new_value = v.clone(); + if let Some(values) = new_value.as_array_mut() { + for v in values { + let data_rewrite = DataRewrite::KeyRenamer({ + DataKeyRenamer { + path: data_path.clone(), + rename_key_to: item.rename_key_to.clone(), + } + }); + data_rewrite.maybe_apply(self.schema, v); + } + } else { + let data_rewrite = DataRewrite::KeyRenamer({ + DataKeyRenamer { + path: data_path.clone(), + rename_key_to: item.rename_key_to.clone(), + } + }); + data_rewrite.maybe_apply(self.schema, &mut new_value); + } + return Some((item.rename_key_to.to_string(), new_value)); + } + } + } + None + } + DataRewrite::ValueSetter(_) => None, + } + }) + .collect(); + self.named_args.push(hash_map); + } + + // Once all a value has been extracted for every variable, go ahead and add all + // variables to the variables map. Additionally, return a ContextualArguments structure if + // values of variables are entity dependent + pub(crate) fn add_variables_and_get_args( + &self, + variables: &mut Map, + ) -> Option { + let (extended_vars, contextual_args) = if let Some(first_map) = self.named_args.first() { + if self.named_args.iter().all(|map| map == first_map) { + ( + first_map + .iter() + .map(|(k, v)| (k.as_str().into(), v.clone())) + .collect(), + None, + ) + } else { + let mut hash_map: HashMap = HashMap::new(); + let arg_names: HashSet<_> = first_map.keys().cloned().collect(); + for (index, item) in self.named_args.iter().enumerate() { + // append _ to each of the arguments and push all the values into hash_map + hash_map.extend(item.iter().map(|(k, v)| { + let mut new_named_param = k.clone(); + new_named_param.push_str(&format!("_{}", index)); + (new_named_param, v.clone()) + })); + } + ( + hash_map, + Some(ContextualArguments { + arguments: arg_names, + count: self.named_args.len(), + }), + ) + } + } else { + (HashMap::new(), None) + }; + + variables.extend( + extended_vars + .iter() + .map(|(key, value)| (key.as_str().into(), value.clone())), + ); + + contextual_args + } +} + +// Take the existing subgraph operation and rewrite it to use aliasing. This will occur in the case +// where we are collecting entites and different entities may have different variables passed to the resolver. +pub(crate) fn build_operation_with_aliasing( + subgraph_operation: &SubgraphOperation, + contextual_arguments: &ContextualArguments, + subgraph_schema: &Valid, +) -> Result, ContextBatchingError> { + let ContextualArguments { arguments, count } = contextual_arguments; + let parsed_document = subgraph_operation.as_parsed(); + + let mut ed = ExecutableDocument::new(); + + // for every operation in the document, go ahead and transform even though it's likely that only one exists + if let Ok(document) = parsed_document { + if let Some(anonymous_op) = &document.anonymous_operation { + let mut cloned = anonymous_op.clone(); + transform_operation(&mut cloned, arguments, count)?; + ed.insert_operation(cloned); + } + + for (_, op) in &document.named_operations { + let mut cloned = op.clone(); + transform_operation(&mut cloned, arguments, count)?; + ed.insert_operation(cloned); + } + + return ed + .validate(subgraph_schema) + .map_err(ContextBatchingError::InvalidDocumentGenerated); + } + Err(ContextBatchingError::NoSelectionSet) +} + +fn transform_operation( + operation: &mut Node, + arguments: &HashSet, + count: &usize, +) -> Result<(), ContextBatchingError> { + let mut selections: Vec = vec![]; + let mut new_variables: Vec> = vec![]; + operation.variables.iter().for_each(|v| { + if arguments.contains(v.name.as_str()) { + for i in 0..*count { + new_variables.push(Node::new(VariableDefinition { + name: Name::new_unchecked(format!("{}_{}", v.name.as_str(), i).into()), + ty: v.ty.clone(), + default_value: v.default_value.clone(), + directives: v.directives.clone(), + })); + } + } else { + new_variables.push(v.clone()); + } + }); + + // there should only be one selection that is a field selection that we're going to rename, but let's count to be sure + // and error if that's not the case + // also it's possible that there could be an inline fragment, so if that's the case, just add those to the new selections once + let mut field_selection: Option> = None; + for selection in &operation.selection_set.selections { + match selection { + Selection::Field(f) => { + if field_selection.is_some() { + // if we get here, there is more than one field selection, which should not be the case + // at the top level of a _entities selection set + return Err(ContextBatchingError::UnexpectedSelection); + } + field_selection = Some(f.clone()); + } + _ => { + // again, if we get here, something is wrong. _entities selection sets should have just one field selection + return Err(ContextBatchingError::UnexpectedSelection); + } + } + } + + let field_selection = field_selection.ok_or(ContextBatchingError::UnexpectedSelection)?; + + for i in 0..*count { + // If we are aliasing, we know that there is only one selection in the top level SelectionSet + // it is a field selection for _entities, so it's ok to reach in and give it an alias + let mut cloned = field_selection.clone(); + let cfs = cloned.make_mut(); + cfs.alias = Some(Name::new_unchecked(format!("_{}", i).into())); + + transform_field_arguments(&mut cfs.arguments, arguments, i); + transform_selection_set(&mut cfs.selection_set, arguments, i); + selections.push(Selection::Field(cloned)); + } + let operation = operation.make_mut(); + operation.variables = new_variables; + operation.selection_set = SelectionSet { + ty: operation.selection_set.ty.clone(), + selections, + }; + Ok(()) +} + +// This function will take the selection set (which has been cloned from the original) +// and transform it so that all contextual variables in the selection set will be appended with a _ +// to match the index in the alias that it is +fn transform_selection_set( + selection_set: &mut SelectionSet, + arguments: &HashSet, + index: usize, +) { + selection_set + .selections + .iter_mut() + .for_each(|selection| match selection { + executable::Selection::Field(node) => { + let node = node.make_mut(); + transform_field_arguments(&mut node.arguments, arguments, index); + transform_selection_set(&mut node.selection_set, arguments, index); + } + executable::Selection::InlineFragment(node) => { + let node = node.make_mut(); + transform_selection_set(&mut node.selection_set, arguments, index); + } + _ => (), + }); +} + +// transforms the variable name on the field argment +fn transform_field_arguments( + arguments_in_selection: &mut [Node], + arguments: &HashSet, + index: usize, +) { + arguments_in_selection.iter_mut().for_each(|arg| { + let arg = arg.make_mut(); + if let Some(v) = arg.value.as_variable() { + if arguments.contains(v.as_str()) { + arg.value = Node::new(ast::Value::Variable(Name::new_unchecked( + format!("{}_{}", v.as_str(), index).into(), + ))); + } + } + }); +} + +#[derive(Debug)] +pub(crate) enum ContextBatchingError { + NoSelectionSet, + InvalidDocumentGenerated(WithErrors), + InvalidRelativePath, + UnexpectedSelection, +} + +#[cfg(test)] +mod subgraph_context_unit_tests { + use super::*; + + #[test] + fn test_merge_context_path() { + let current_dir: Path = serde_json::from_str(r#"["t","u"]"#).unwrap(); + let relative_path: Path = serde_json::from_str(r#"["..","... on T","prop"]"#).unwrap(); + let expected = r#"["t","... on T","prop"]"#; + + let result = merge_context_path(¤t_dir, &relative_path).unwrap(); + assert_eq!(expected, serde_json::to_string(&result).unwrap(),); + } + + #[test] + fn test_merge_context_path_invalid() { + let current_dir: Path = serde_json::from_str(r#"["t","u"]"#).unwrap(); + let relative_path: Path = + serde_json::from_str(r#"["..","..","..","... on T","prop"]"#).unwrap(); + + let result = merge_context_path(¤t_dir, &relative_path); + match result { + Ok(_) => panic!("Expected an error, but got Ok"), + Err(e) => match e { + ContextBatchingError::InvalidRelativePath => (), + _ => panic!("Expected InvalidRelativePath, but got a different error"), + }, + } + } + + #[test] + fn test_transform_selection_set() { + let type_name = executable::Name::new("Hello").unwrap(); + let field_name = executable::Name::new("f").unwrap(); + let field_definition = ast::FieldDefinition { + description: None, + name: field_name.clone(), + arguments: vec![Node::new(ast::InputValueDefinition { + description: None, + name: executable::Name::new("param").unwrap(), + ty: Node::new(ast::Type::Named( + executable::Name::new("ParamType").unwrap(), + )), + default_value: None, + directives: ast::DirectiveList(vec![]), + })], + ty: ast::Type::Named(executable::Name::new("FieldType").unwrap()), + directives: ast::DirectiveList(vec![]), + }; + let mut selection_set = SelectionSet::new(type_name); + let field = executable::Field::new( + executable::Name::new("f").unwrap(), + Node::new(field_definition), + ) + .with_argument( + executable::Name::new("param").unwrap(), + Node::new(ast::Value::Variable( + executable::Name::new("variable").unwrap(), + )), + ); + + selection_set.push(Selection::Field(Node::new(field))); + + // before modifications + assert_eq!( + "{ f(param: $variable) }", + selection_set.serialize().no_indent().to_string() + ); + + let mut hash_set = HashSet::new(); + + // create a hash set that will miss completely. transform has no effect + hash_set.insert("one".to_string()); + hash_set.insert("two".to_string()); + hash_set.insert("param".to_string()); + let mut clone = selection_set.clone(); + transform_selection_set(&mut clone, &hash_set, 7); + assert_eq!( + "{ f(param: $variable) }", + clone.serialize().no_indent().to_string() + ); + + // add variable that will hit and cause a rewrite + hash_set.insert("variable".to_string()); + let mut clone = selection_set.clone(); + transform_selection_set(&mut clone, &hash_set, 7); + assert_eq!( + "{ f(param: $variable_7) }", + clone.serialize().no_indent().to_string() + ); + + // add_alias = true will add a "_3:" alias + let clone = selection_set.clone(); + let mut operation = Node::new(executable::Operation { + operation_type: executable::OperationType::Query, + name: None, + variables: vec![], + directives: ast::DirectiveList(vec![]), + selection_set: clone, + }); + let count = 3; + transform_operation(&mut operation, &hash_set, &count).unwrap(); + assert_eq!( + "{ _0: f(param: $variable_0) _1: f(param: $variable_1) _2: f(param: $variable_2) }", + operation.serialize().no_indent().to_string() + ); + } +} diff --git a/apollo-router/src/query_planner/subscription.rs b/apollo-router/src/query_planner/subscription.rs index caf90730e6..6a327fdc60 100644 --- a/apollo-router/src/query_planner/subscription.rs +++ b/apollo-router/src/query_planner/subscription.rs @@ -208,6 +208,7 @@ impl SubscriptionNode { parameters.supergraph_request, parameters.schema, &self.input_rewrites, + &None, ) { Some(variables) => variables, None => { diff --git a/apollo-router/src/query_planner/tests.rs b/apollo-router/src/query_planner/tests.rs index 3431fda2b5..8c88e16a4e 100644 --- a/apollo-router/src/query_planner/tests.rs +++ b/apollo-router/src/query_planner/tests.rs @@ -116,6 +116,7 @@ async fn mock_subgraph_service_withf_panics_should_be_reported_as_service_closed &sf, &Default::default(), &Arc::new(Schema::parse_test(test_schema!(), &Default::default()).unwrap()), + &Default::default(), sender, None, &None, @@ -177,6 +178,7 @@ async fn fetch_includes_operation_name() { &sf, &Default::default(), &Arc::new(Schema::parse_test(test_schema!(), &Default::default()).unwrap()), + &Default::default(), sender, None, &None, @@ -235,6 +237,7 @@ async fn fetch_makes_post_requests() { &sf, &Default::default(), &Arc::new(Schema::parse_test(test_schema!(), &Default::default()).unwrap()), + &Default::default(), sender, None, &None, @@ -266,6 +269,7 @@ async fn defer() { id: Some("fetch1".into()), input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: Default::default(), authorization: Default::default(), }))), @@ -311,6 +315,7 @@ async fn defer() { id: Some("fetch2".into()), input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: Default::default(), authorization: Default::default(), })), @@ -385,6 +390,7 @@ async fn defer() { &sf, &Default::default(), &schema, + &Default::default(), sender, None, &None, @@ -493,6 +499,7 @@ async fn defer_if_condition() { .unwrap(), ), &schema, + &Default::default(), sender, None, &None, @@ -515,6 +522,7 @@ async fn defer_if_condition() { &service_factory, &Default::default(), &schema, + &Default::default(), default_sender, None, &None, @@ -546,6 +554,7 @@ async fn defer_if_condition() { .unwrap(), ), &schema, + &Default::default(), sender, None, &None, @@ -667,6 +676,7 @@ async fn dependent_mutations() { &sf, &Default::default(), &Arc::new(Schema::parse_test(schema, &Default::default()).unwrap()), + &Default::default(), sender, None, &None, @@ -1799,6 +1809,7 @@ fn broken_plan_does_not_panic() { id: Some("fetch1".into()), input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: Default::default(), authorization: Default::default(), }), @@ -1813,7 +1824,9 @@ fn broken_plan_does_not_panic() { let subgraph_schema = apollo_compiler::Schema::parse_and_validate(subgraph_schema, "").unwrap(); let mut subgraph_schemas = HashMap::new(); subgraph_schemas.insert("X".to_owned(), Arc::new(subgraph_schema)); - let result = plan.root.hash_subqueries(&subgraph_schemas, ""); + let result = plan + .root + .init_parsed_operations_and_hash_subqueries(&subgraph_schemas, ""); assert_eq!( result.unwrap_err().to_string(), r#"[1:3] Cannot query field "invalid" on type "Query"."# diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index 85bef7ea84..eeadf88799 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -664,7 +664,7 @@ pub(crate) async fn create_plugins( // This relative ordering is documented in `docs/source/customizations/native.mdx`: add_optional_apollo_plugin!("rhai"); add_optional_apollo_plugin!("coprocessor"); - add_optional_apollo_plugin!("experimental_demand_control"); + add_optional_apollo_plugin!("preview_demand_control"); add_user_plugins!(); // Macros above remove from `apollo_plugin_factories`, so anything left at the end diff --git a/apollo-router/src/services/execution/service.rs b/apollo-router/src/services/execution/service.rs index ddd9f96ca6..fbde3de97a 100644 --- a/apollo-router/src/services/execution/service.rs +++ b/apollo-router/src/services/execution/service.rs @@ -57,6 +57,7 @@ use crate::spec::Schema; #[derive(Clone)] pub(crate) struct ExecutionService { pub(crate) schema: Arc, + pub(crate) subgraph_schemas: Arc>>>, pub(crate) subgraph_service_factory: Arc, /// Subscription config if enabled subscription_config: Option, @@ -148,6 +149,7 @@ impl ExecutionService { &self.subgraph_service_factory, &Arc::new(req.supergraph_request), &self.schema, + &self.subgraph_schemas, sender, subscription_handle.clone(), &self.subscription_config, @@ -618,6 +620,7 @@ impl ServiceFactory for ExecutionServiceFactory { schema: self.schema.clone(), subgraph_service_factory: self.subgraph_service_factory.clone(), subscription_config: subscription_plugin_conf, + subgraph_schemas: self.subgraph_schemas.clone(), } .boxed(), |acc, (_, e)| e.execution_service(acc), diff --git a/apollo-router/src/services/layers/apq.rs b/apollo-router/src/services/layers/apq.rs index fade1bccde..57d853b162 100644 --- a/apollo-router/src/services/layers/apq.rs +++ b/apollo-router/src/services/layers/apq.rs @@ -116,7 +116,12 @@ async fn apq_request( } } (Some((apq_hash, _)), _) => { - if let Ok(cached_query) = cache.get(&redis_key(&apq_hash)).await.get().await { + if let Ok(cached_query) = cache + .get(&redis_key(&apq_hash), |_| Ok(())) + .await + .get() + .await + { let _ = request.context.insert("persisted_query_hit", true); tracing::trace!("apq: cache hit"); request.supergraph_request.body_mut().query = Some(cached_query); diff --git a/apollo-router/src/services/supergraph/service.rs b/apollo-router/src/services/supergraph/service.rs index dc5807f272..9a270c2f1c 100644 --- a/apollo-router/src/services/supergraph/service.rs +++ b/apollo-router/src/services/supergraph/service.rs @@ -763,9 +763,11 @@ impl PluggableSupergraphServiceBuilder { let configuration = self.configuration.unwrap_or_default(); let schema = self.planner.schema(); + let subgraph_schemas = self.planner.subgraph_schemas(); let query_planner_service = CachingQueryPlanner::new( self.planner, schema.clone(), + subgraph_schemas, &configuration, IndexMap::new(), ) diff --git a/apollo-router/src/uplink/license_enforcement.rs b/apollo-router/src/uplink/license_enforcement.rs index 50b6c410c6..1568881581 100644 --- a/apollo-router/src/uplink/license_enforcement.rs +++ b/apollo-router/src/uplink/license_enforcement.rs @@ -378,6 +378,10 @@ impl LicenseEnforcementReport { .path("$.telemetry..instruments") .name("Advanced telemetry") .build(), + ConfigurationRestriction::builder() + .path("$.telemetry..graphql") + .name("Advanced telemetry") + .build(), ConfigurationRestriction::builder() .path("$.preview_file_uploads") .name("File uploads plugin") @@ -387,7 +391,7 @@ impl LicenseEnforcementReport { .name("Batching support") .build(), ConfigurationRestriction::builder() - .path("$.experimental_demand_control") + .path("$.preview_demand_control") .name("Demand control plugin") .build(), ] @@ -408,6 +412,19 @@ impl LicenseEnforcementReport { }], }, }, + SchemaRestriction::Spec { + name: "context".to_string(), + spec_url: "https://specs.apollo.dev/context".to_string(), + version_req: semver::VersionReq { + comparators: vec![semver::Comparator { + op: semver::Op::Exact, + major: 0, + minor: 1.into(), + patch: 0.into(), + pre: semver::Prerelease::EMPTY, + }], + }, + }, SchemaRestriction::Spec { name: "requiresScopes".to_string(), spec_url: "https://specs.apollo.dev/requiresScopes".to_string(), @@ -436,6 +453,21 @@ impl LicenseEnforcementReport { }, explanation: "The `overrideLabel` argument on the join spec's @field directive is restricted to Enterprise users. This argument exists in your supergraph as a result of using the `@override` directive with the `label` argument in one or more of your subgraphs.".to_string() }, + SchemaRestriction::DirectiveArgument { + name: "field".to_string(), + argument: "contextArguments".to_string(), + spec_url: "https://specs.apollo.dev/join".to_string(), + version_req: semver::VersionReq { + comparators: vec![semver::Comparator { + op: semver::Op::GreaterEq, + major: 0, + minor: 5.into(), + patch: 0.into(), + pre: semver::Prerelease::EMPTY, + }], + }, + explanation: "The `contextArguments` argument on the join spec's @field directive is restricted to Enterprise users. This argument exists in your supergraph as a result of using the `@fromContext` directive in one or more of your subgraphs.".to_string() + }, ] } } @@ -785,6 +817,20 @@ mod test { assert_snapshot!(report.to_string()); } + #[test] + fn set_context() { + let report = check( + include_str!("testdata/oss.router.yaml"), + include_str!("testdata/set_context.graphql"), + ); + + assert!( + !report.restricted_schema_in_use.is_empty(), + "should have found restricted features" + ); + assert_snapshot!(report.to_string()); + } + #[test] fn progressive_override_with_renamed_join_spec() { let report = check( diff --git a/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__restricted_features_via_config.snap b/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__restricted_features_via_config.snap index 83da9acff5..cc488786dd 100644 --- a/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__restricted_features_via_config.snap +++ b/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__restricted_features_via_config.snap @@ -45,8 +45,14 @@ Configuration yaml: * Advanced telemetry .telemetry..spans.subgraph +* Advanced telemetry + .telemetry..instruments + +* Advanced telemetry + .telemetry..graphql + * File uploads plugin .preview_file_uploads * Demand control plugin - .experimental_demand_control + .preview_demand_control diff --git a/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__set_context.snap b/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__set_context.snap new file mode 100644 index 0000000000..79ea2bbce0 --- /dev/null +++ b/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__set_context.snap @@ -0,0 +1,12 @@ +--- +source: apollo-router/src/uplink/license_enforcement.rs +expression: report.to_string() +--- +Schema features: +* @context + https://specs.apollo.dev/context/v0.1 + +* @join__field.contextArguments + https://specs.apollo.dev/join/v0.5 + +The `contextArguments` argument on the join spec's @field directive is restricted to Enterprise users. This argument exists in your supergraph as a result of using the `@fromContext` directive in one or more of your subgraphs. diff --git a/apollo-router/src/uplink/testdata/restricted.router.yaml b/apollo-router/src/uplink/testdata/restricted.router.yaml index ab99114f4e..ce6297634f 100644 --- a/apollo-router/src/uplink/testdata/restricted.router.yaml +++ b/apollo-router/src/uplink/testdata/restricted.router.yaml @@ -70,6 +70,9 @@ telemetry: subgraph: attributes: subgraph.graphql.document: true + instruments: + graphql: + list.length: true preview_file_uploads: enabled: true @@ -78,7 +81,7 @@ preview_file_uploads: enabled: true mode: stream -experimental_demand_control: +preview_demand_control: enabled: true mode: measure strategy: diff --git a/apollo-router/src/uplink/testdata/set_context.graphql b/apollo-router/src/uplink/testdata/set_context.graphql new file mode 100644 index 0000000000..16b9ba0019 --- /dev/null +++ b/apollo-router/src/uplink/testdata/set_context.graphql @@ -0,0 +1,165 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", for: SECURITY) { + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @join__directive( + graphs: [join__Graph!] + name: String! + args: join__DirectiveArguments +) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + overrideLabel: String + contextArguments: [join__ContextArgument!] +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar context__context + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "https://Subgraph1") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "https://Subgraph2") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query @join__type(graph: SUBGRAPH1) @join__type(graph: SUBGRAPH2) { + t: T! @join__field(graph: SUBGRAPH1) + tList: [T]! @join__field(graph: SUBGRAPH1) + a: Int! @join__field(graph: SUBGRAPH2) + k: K! @join__field(graph: SUBGRAPH1) +} + +union K + @join__type(graph: SUBGRAPH1) + @join__unionMember(graph: SUBGRAPH1, member: "A") + @join__unionMember(graph: SUBGRAPH1, member: "B") + @context(name: "Subgraph1__context2") = + A + | B + +type A @join__type(graph: SUBGRAPH1, key: "id") { + id: ID! + v: V! + prop: String! +} + +type B @join__type(graph: SUBGRAPH1, key: "id") { + id: ID! + v: V! + prop: String! +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") { + id: ID! + u: U! + uList: [U]! + prop: String! +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") { + id: ID! + b: String! @join__field(graph: SUBGRAPH2) + field: Int! + @join__field( + graph: SUBGRAPH1 + contextArguments: [ + { + context: "Subgraph1__context" + name: "a" + type: "String" + selection: "{ prop }" + } + ] + ) +} + +type V + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") { + id: ID! + b: String! @join__field(graph: SUBGRAPH2) + field: Int! + @join__field( + graph: SUBGRAPH1 + contextArguments: [ + { + context: "Subgraph1__context2" + name: "a" + type: "String" + selection: "... on A { prop } ... on B { prop }" + } + ] + ) +} diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index c2ad6bca3d..143be91bc9 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -830,6 +830,40 @@ impl IntegrationTest { panic!("'{text}' not detected in metrics\n{last_metrics}"); } + #[allow(dead_code)] + pub async fn assert_metrics_contains_multiple( + &self, + mut texts: Vec<&str>, + duration: Option, + ) { + let now = Instant::now(); + let mut last_metrics = String::new(); + while now.elapsed() < duration.unwrap_or_else(|| Duration::from_secs(15)) { + if let Ok(metrics) = self + .get_metrics_response() + .await + .expect("failed to fetch metrics") + .text() + .await + { + let mut v = vec![]; + for text in &texts { + if !metrics.contains(text) { + v.push(*text); + } + } + if v.len() == texts.len() { + return; + } else { + texts = v; + } + last_metrics = metrics; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + panic!("'{texts:?}' not detected in metrics\n{last_metrics}"); + } + #[allow(dead_code)] pub async fn assert_metrics_does_not_contain(&self, text: &str) { if let Ok(metrics) = self diff --git a/apollo-router/tests/fixtures/set_context/one.json b/apollo-router/tests/fixtures/set_context/one.json new file mode 100644 index 0000000000..e552a0f47b --- /dev/null +++ b/apollo-router/tests/fixtures/set_context/one.json @@ -0,0 +1,414 @@ +{ + "mocks": [ + { + "request": { + "query": "query Query__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query__Subgraph1__0{t{__typename prop id uList{__typename id}}}", + "operationName": "Query__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "uList": [ + { + "__typename": "U", + "id": "1" + }, + { + "__typename": "U", + "id": "2" + }, + { + "__typename": "U", + "id": "3" + } + ] + } + } + } + }, + { + "request": { + "query": "query QueryLL__Subgraph1__0{tList{__typename prop id uList{__typename id}}}", + "operationName": "QueryLL__Subgraph1__0" + }, + "response": { + "data": { + "tList": [ + { + "__typename": "T", + "prop": "prop value 1", + "id": "1", + "uList": [ + { + "__typename": "U", + "id": "3" + } + ] + }, + { + "__typename": "T", + "prop": "prop value 2", + "id": "2", + "uList": [ + { + "__typename": "U", + "id": "4" + } + ] + } + ] + } + } + }, + { + "request": { + "query": "query QueryUnion__Subgraph1__0{k{__typename ...on A{__typename prop v{__typename id}}...on B{__typename prop v{__typename id}}}}", + "operationName": "QueryUnion__Subgraph1__0" + }, + "response": { + "data": { + "k": { + "__typename": "A", + "prop": "prop value 3", + "id": 1, + "v": { + "__typename": "V", + "id": "2" + } + } + } + } + }, + { + "request": { + "query": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query__Subgraph1__1", + "variables": { + "contextualArgument_1_0": "prop value", + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "1", + "field": 1234 + } + ] + } + } + }, + { + "request": { + "query": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query__Subgraph1__1", + "variables": { + "contextualArgument_1_0": "prop value", + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "_entities": [ + { + "__typename": "U", + "id": "1", + "field": 1234 + } + ] + } + } + }, + { + "request": { + "query": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query__Subgraph1__1", + "variables": { + "contextualArgument_1_0": "prop value", + "representations": [ + { "__typename": "U", "id": "1" }, + { "__typename": "U", "id": "2" }, + { "__typename": "U", "id": "3" } + ] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "1", + "field": 1234 + }, + { + "id": "2", + "field": 2345 + }, + { + "id": "3", + "field": 3456 + } + ] + } + } + }, + { + "request": { + "query": "query QueryLL__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_0_0: String, $contextualArgument_1_0_1: String) { _0: _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0_0) } } _1: _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0_1) } } }", + "operationName": "QueryLL__Subgraph1__1", + "variables": { + "contextualArgument_1_0_0": "prop value 1", + "contextualArgument_1_0_1": "prop value 2", + "representations": [ + { "__typename": "U", "id": "3" }, + { "__typename": "U", "id": "4" } + ] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "3", + "field": 3456 + }, + { + "id": "4", + "field": 4567 + } + ] + } + } + }, + { + "request": { + "query": "query QueryLL__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_0_0: String, $contextualArgument_1_0_1: String) { _0: _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0_0) } } _1: _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0_1) } } }", + "operationName": "QueryLL__Subgraph1__1", + "variables": { + "contextualArgument_1_0_1": "prop value 2", + "contextualArgument_1_0_0": "prop value 1", + "representations": [ + { "__typename": "U", "id": "3" }, + { "__typename": "U", "id": "4" } + ] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "3", + "field": 3456 + }, + { + "id": "4", + "field": 4567 + } + ] + } + } + }, + { + "request": { + "query": "query QueryUnion__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_1:String){_entities(representations:$representations){...on V{field(a:$contextualArgument_1_1)}}}", + "operationName": "QueryUnion__Subgraph1__1", + "variables": { + "contextualArgument_1_1": "prop value 3", + "representations": [{ "__typename": "V", "id": "2" }] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "3", + "field": 3456 + } + ] + } + } + }, + { + "request": { + "query": "query Query_Null_Param__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_Null_Param__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": null, + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query_Null_Param__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query_Null_Param__Subgraph1__1", + "variables": { + "contextualArgument_1_0": null, + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "1", + "field": 1234 + } + ] + } + } + }, + { + "request": { + "query": "query Query_type_mismatch__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_type_mismatch__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": 7, + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query_type_mismatch__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query_type_mismatch__Subgraph1__1", + "variables": { + "contextualArgument_1_0": 7, + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "1", + "field": 1234 + } + ] + } + } + }, + { + "request": { + "query": "query Query_fetch_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_fetch_failure__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query_fetch_failure__Subgraph1__2($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query_fetch_failure__Subgraph1__2", + "variables": { + "contextualArgument_1_0": "prop value", + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query_fetch_dependent_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_fetch_dependent_failure__Subgraph1__0" + }, + "response": { + "response": { + "data": null, + "errors": [{ + "message": "Some error", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": ["t", "u"] + }] + } + } + } + ] +} diff --git a/apollo-router/tests/fixtures/set_context/supergraph.graphql b/apollo-router/tests/fixtures/set_context/supergraph.graphql new file mode 100644 index 0000000000..16b9ba0019 --- /dev/null +++ b/apollo-router/tests/fixtures/set_context/supergraph.graphql @@ -0,0 +1,165 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", for: SECURITY) { + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @join__directive( + graphs: [join__Graph!] + name: String! + args: join__DirectiveArguments +) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + overrideLabel: String + contextArguments: [join__ContextArgument!] +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar context__context + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "https://Subgraph1") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "https://Subgraph2") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query @join__type(graph: SUBGRAPH1) @join__type(graph: SUBGRAPH2) { + t: T! @join__field(graph: SUBGRAPH1) + tList: [T]! @join__field(graph: SUBGRAPH1) + a: Int! @join__field(graph: SUBGRAPH2) + k: K! @join__field(graph: SUBGRAPH1) +} + +union K + @join__type(graph: SUBGRAPH1) + @join__unionMember(graph: SUBGRAPH1, member: "A") + @join__unionMember(graph: SUBGRAPH1, member: "B") + @context(name: "Subgraph1__context2") = + A + | B + +type A @join__type(graph: SUBGRAPH1, key: "id") { + id: ID! + v: V! + prop: String! +} + +type B @join__type(graph: SUBGRAPH1, key: "id") { + id: ID! + v: V! + prop: String! +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") { + id: ID! + u: U! + uList: [U]! + prop: String! +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") { + id: ID! + b: String! @join__field(graph: SUBGRAPH2) + field: Int! + @join__field( + graph: SUBGRAPH1 + contextArguments: [ + { + context: "Subgraph1__context" + name: "a" + type: "String" + selection: "{ prop }" + } + ] + ) +} + +type V + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") { + id: ID! + b: String! @join__field(graph: SUBGRAPH2) + field: Int! + @join__field( + graph: SUBGRAPH1 + contextArguments: [ + { + context: "Subgraph1__context2" + name: "a" + type: "String" + selection: "... on A { prop } ... on B { prop }" + } + ] + ) +} diff --git a/apollo-router/tests/fixtures/set_context/two.json b/apollo-router/tests/fixtures/set_context/two.json new file mode 100644 index 0000000000..6ab18d052b --- /dev/null +++ b/apollo-router/tests/fixtures/set_context/two.json @@ -0,0 +1,43 @@ +{ + "mocks": [ + { + "request": { + "query": "query Query__two__2($representations:[_Any!]!){_entities(representations:$representations){...on U{k}}}", + "operationName": "Query__two__2", + "variables": { "representations": [{ "__typename": "U", "id": "1" }] } + }, + "response": { + "data": { + "_entities": [ + { + "k": "k value" + } + ] + } + } + }, + { + "request": { + "query": "query Query_fetch_failure__Subgraph2__1($representations:[_Any!]!){_entities(representations:$representations){...on U{b}}}", + "operationName": "Query_fetch_failure__Subgraph2__1", + "variables": { + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": null, + "errors": [{ + "message": "Some error", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": ["t", "u"] + } + ] + } + } + ] +} diff --git a/apollo-router/tests/integration/lifecycle.rs b/apollo-router/tests/integration/lifecycle.rs index 04104cbba6..8294d762ea 100644 --- a/apollo-router/tests/integration/lifecycle.rs +++ b/apollo-router/tests/integration/lifecycle.rs @@ -86,23 +86,21 @@ async fn test_reload_config_with_broken_plugin() -> Result<(), BoxError> { #[tokio::test(flavor = "multi_thread")] async fn test_reload_config_with_broken_plugin_recovery() -> Result<(), BoxError> { - for i in 0..3 { - println!("iteration {i}"); - let mut router = IntegrationTest::builder() - .config(HAPPY_CONFIG) - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - router.update_config(BROKEN_PLUGIN_CONFIG).await; - router.assert_not_reloaded().await; - router.execute_default_query().await; - router.update_config(HAPPY_CONFIG).await; - router.assert_reloaded().await; - router.execute_default_query().await; - router.graceful_shutdown().await; - } + let mut router = IntegrationTest::builder() + .config(HAPPY_CONFIG) + .build() + .await; + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + router.update_config(BROKEN_PLUGIN_CONFIG).await; + router.assert_not_reloaded().await; + router.execute_default_query().await; + router.update_config(HAPPY_CONFIG).await; + router.assert_reloaded().await; + router.execute_default_query().await; + router.graceful_shutdown().await; + Ok(()) } diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index 4b3aa46d78..1d1d5bfd73 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -28,7 +28,7 @@ async fn query_planner() -> Result<(), BoxError> { // 2. run `docker compose up -d` and connect to the redis container by running `docker-compose exec redis /bin/bash`. // 3. Run the `redis-cli` command from the shell and start the redis `monitor` command. // 4. Run this test and yank the updated cache key from the redis logs. - let known_cache_key = "plan:0:v2.7.5:16385ebef77959fcdc520ad507eb1f7f7df28f1d54a0569e3adabcb4cd00d7ce:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:9c26cb1f820a78848ba3d5d3295c16aa971368c5295422fd33cc19d4a6006a9c"; + let known_cache_key = "plan:0:v2.8.0:16385ebef77959fcdc520ad507eb1f7f7df28f1d54a0569e3adabcb4cd00d7ce:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:9c26cb1f820a78848ba3d5d3295c16aa971368c5295422fd33cc19d4a6006a9c"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -902,7 +902,7 @@ async fn connection_failure_blocks_startup() { async fn query_planner_redis_update_query_fragments() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:0:v2.7.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:ae8b525534cb7446a34715fc80edd41d4d29aa65c5f39f9237d4ed8459e3fe82", + "plan:0:v2.8.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:ae8b525534cb7446a34715fc80edd41d4d29aa65c5f39f9237d4ed8459e3fe82", ) .await; } @@ -921,7 +921,7 @@ async fn query_planner_redis_update_planner_mode() { async fn query_planner_redis_update_introspection() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_introspection.router.yaml"), - "plan:0:v2.7.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:1910d63916aae7a1066cb8c7d622fc3a8e363ed1b6ac8e214deed4046abae85c", + "plan:0:v2.8.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:1910d63916aae7a1066cb8c7d622fc3a8e363ed1b6ac8e214deed4046abae85c", ) .await; } @@ -930,7 +930,7 @@ async fn query_planner_redis_update_introspection() { async fn query_planner_redis_update_defer() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:0:v2.7.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8a17c5b196af5e3a18d24596424e9849d198f456dd48297b852a5f2ca847169b", + "plan:0:v2.8.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8a17c5b196af5e3a18d24596424e9849d198f456dd48297b852a5f2ca847169b", ) .await; } @@ -941,7 +941,7 @@ async fn query_planner_redis_update_type_conditional_fetching() { include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:0:v2.7.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:275f78612ed3d45cdf6bf328ef83e368b5a44393bd8c944d4a7d694aed61f017", + "plan:0:v2.8.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:275f78612ed3d45cdf6bf328ef83e368b5a44393bd8c944d4a7d694aed61f017", ) .await; } @@ -952,7 +952,7 @@ async fn query_planner_redis_update_reuse_query_fragments() { include_str!( "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" ), - "plan:0:v2.7.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:15fbb62c94e8da6ea78f28a6eb86a615dcaf27ff6fd0748fac4eb614b0b17662", + "plan:0:v2.8.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:15fbb62c94e8da6ea78f28a6eb86a615dcaf27ff6fd0748fac4eb614b0b17662", ) .await; } @@ -972,7 +972,7 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key router.assert_started().await; router.clear_redis_cache().await; - let starting_key = "plan:0:v2.7.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:1910d63916aae7a1066cb8c7d622fc3a8e363ed1b6ac8e214deed4046abae85c"; + let starting_key = "plan:0:v2.8.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:1910d63916aae7a1066cb8c7d622fc3a8e363ed1b6ac8e214deed4046abae85c"; router.execute_default_query().await; router.assert_redis_cache_contains(starting_key, None).await; router.update_config(updated_config).await; diff --git a/apollo-router/tests/integration/rhai.rs b/apollo-router/tests/integration/rhai.rs index b4bf3e2846..d94a47c567 100644 --- a/apollo-router/tests/integration/rhai.rs +++ b/apollo-router/tests/integration/rhai.rs @@ -9,7 +9,7 @@ use tower::ServiceExt; #[tokio::test(flavor = "multi_thread")] async fn all_rhai_callbacks_are_invoked() { let env_filter = "apollo_router=info"; - let mock_writer = tracing_test::internal::MockWriter::new(&tracing_test::internal::GLOBAL_BUF); + let mock_writer = tracing_test::internal::MockWriter::new(tracing_test::internal::global_buf()); let subscriber = tracing_test::internal::get_subscriber(mock_writer, env_filter); let _guard = tracing::dispatcher::set_default(&subscriber); diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner.snap index 4714fe2240..f90305be82 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner.snap @@ -12,6 +12,7 @@ expression: query_plan "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "121b9859eba2d8fa6dde0a54b6e3781274cf69f7ffb0af912e92c01c6bfff6ca", "authorization": { "is_authenticated": false, diff --git a/apollo-router/tests/integration/telemetry/fixtures/graphql.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/graphql.router.yaml new file mode 100644 index 0000000000..36d1ce4fe6 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/graphql.router.yaml @@ -0,0 +1,45 @@ +telemetry: + exporters: + tracing: + propagation: + trace_context: true + metrics: + prometheus: + enabled: true + + instrumentation: + instruments: + graphql: + field.execution: true + list.length: true + "custom_counter": + description: "count of name field" + type: counter + unit: "unit" + value: field_unit + attributes: + graphql.type.name: true + graphql.field.type: true + graphql.field.name: true + condition: + eq: + - field_name: string + - "name" + "custom_histogram": + description: "histogram of review length" + type: histogram + unit: "unit" + attributes: + graphql.type.name: true + graphql.field.type: true + graphql.field.name: true + value: + field_custom: + list_length: value + condition: + eq: + - field_name: string + - "topProducts" + + + diff --git a/apollo-router/tests/integration/telemetry/fixtures/jaeger-advanced.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/jaeger-advanced.router.yaml index 28cff61731..34f483eacf 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/jaeger-advanced.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/jaeger-advanced.router.yaml @@ -44,6 +44,8 @@ telemetry: eq: - request_header: "head" - "test" + studio.trace.id: + trace_id: apollo supergraph: attributes: graphql.operation.name: true diff --git a/apollo-router/tests/integration/telemetry/jaeger.rs b/apollo-router/tests/integration/telemetry/jaeger.rs index fe5c49f595..2d7e3829de 100644 --- a/apollo-router/tests/integration/telemetry/jaeger.rs +++ b/apollo-router/tests/integration/telemetry/jaeger.rs @@ -412,6 +412,14 @@ fn verify_router_span_fields( .first(), Some(&&Value::String("test".to_string())) ); + assert_eq!( + router_span + .select_path("$.tags[?(@.key == 'studio.trace.id')].value")? + .first(), + Some(&&Value::String( + "f60e643d7f52ecda23216f86409d7e2e5c3aa68c".to_string() + )) + ); } Ok(()) diff --git a/apollo-router/tests/integration/telemetry/metrics.rs b/apollo-router/tests/integration/telemetry/metrics.rs index 26423e5ec7..6e745671f3 100644 --- a/apollo-router/tests/integration/telemetry/metrics.rs +++ b/apollo-router/tests/integration/telemetry/metrics.rs @@ -40,33 +40,54 @@ async fn test_metrics_reloading() { router.assert_reloaded().await; } - router.assert_metrics_contains(r#"apollo_router_cache_hit_count_total{kind="query planner",storage="memory",otel_scope_name="apollo/router"} 4"#, None).await; - router.assert_metrics_contains(r#"apollo_router_cache_miss_count_total{kind="query planner",storage="memory",otel_scope_name="apollo/router"} 2"#, None).await; - router.assert_metrics_contains(r#"apollo_router_http_request_duration_seconds_bucket{status="200",otel_scope_name="apollo/router",le="100"}"#, None).await; - router - .assert_metrics_contains(r#"apollo_router_cache_hit_time"#, None) - .await; - router - .assert_metrics_contains(r#"apollo_router_cache_miss_time"#, None) - .await; - router - .assert_metrics_contains(r#"apollo_router_session_count_total"#, None) - .await; - router - .assert_metrics_contains(r#"custom_header="test_custom""#, None) - .await; + let metrics = router + .get_metrics_response() + .await + .expect("failed to fetch metrics") + .text() + .await + .unwrap(); + + check_metrics_contains( + &metrics, + r#"apollo_router_cache_hit_count_total{kind="query planner",storage="memory",otel_scope_name="apollo/router"} 4"#, + ); + check_metrics_contains( + &metrics, + r#"apollo_router_cache_miss_count_total{kind="query planner",storage="memory",otel_scope_name="apollo/router"} 2"#, + ); + check_metrics_contains( + &metrics, + r#"apollo_router_http_request_duration_seconds_bucket{status="200",otel_scope_name="apollo/router",le="100"}"#, + ); + check_metrics_contains(&metrics, r#"apollo_router_cache_hit_time"#); + check_metrics_contains(&metrics, r#"apollo_router_cache_miss_time"#); + check_metrics_contains(&metrics, r#"apollo_router_session_count_total"#); + check_metrics_contains(&metrics, r#"custom_header="test_custom""#); + router .assert_metrics_does_not_contain(r#"_total_total{"#) .await; if std::env::var("TEST_APOLLO_KEY").is_ok() && std::env::var("TEST_APOLLO_GRAPH_REF").is_ok() { - router.assert_metrics_contains(r#"apollo_router_telemetry_studio_reports_total{report_type="metrics",otel_scope_name="apollo/router"} 2"#, Some(Duration::from_secs(10))).await; - router.assert_metrics_contains(r#"apollo_router_telemetry_studio_reports_total{report_type="traces",otel_scope_name="apollo/router"} 2"#, Some(Duration::from_secs(10))).await; - router.assert_metrics_contains(r#"apollo_router_uplink_fetch_duration_seconds_count{kind="unchanged",query="License",url="https://uplink.api.apollographql.com/",otel_scope_name="apollo/router"}"#, Some(Duration::from_secs(120))).await; - router.assert_metrics_contains(r#"apollo_router_uplink_fetch_count_total{query="License",status="success",otel_scope_name="apollo/router"}"#, Some(Duration::from_secs(1))).await; + router.assert_metrics_contains_multiple(vec![ + r#"apollo_router_telemetry_studio_reports_total{report_type="metrics",otel_scope_name="apollo/router"} 2"#, + r#"apollo_router_telemetry_studio_reports_total{report_type="traces",otel_scope_name="apollo/router"} 2"#, + r#"apollo_router_uplink_fetch_duration_seconds_count{kind="unchanged",query="License",url="https://uplink.api.apollographql.com/",otel_scope_name="apollo/router"}"#, + r#"apollo_router_uplink_fetch_count_total{query="License",status="success",otel_scope_name="apollo/router"}"# + ], Some(Duration::from_secs(10))) + .await; } } +#[track_caller] +fn check_metrics_contains(metrics: &str, text: &str) { + assert!( + metrics.contains(text), + "'{text}' not detected in metrics\n{metrics}" + ); +} + #[tokio::test(flavor = "multi_thread")] async fn test_subgraph_auth_metrics() { let mut router = IntegrationTest::builder() @@ -165,3 +186,33 @@ async fn test_bad_queries() { ) .await; } + +#[tokio::test(flavor = "multi_thread")] +async fn test_graphql_metrics() { + let mut router = IntegrationTest::builder() + .config(include_str!("fixtures/graphql.router.yaml")) + .build() + .await; + + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + router + .assert_metrics_contains(r#"graphql_field_list_length_sum{graphql_field_name="topProducts",graphql_field_type="Product",graphql_type_name="Query",otel_scope_name="apollo/router"} 3"#, None) + .await; + router + .assert_metrics_contains(r#"graphql_field_list_length_bucket{graphql_field_name="topProducts",graphql_field_type="Product",graphql_type_name="Query",otel_scope_name="apollo/router",le="5"} 1"#, None) + .await; + router + .assert_metrics_contains(r#"graphql_field_execution_total{graphql_field_name="name",graphql_field_type="String",graphql_type_name="Product",otel_scope_name="apollo/router"} 3"#, None) + .await; + router + .assert_metrics_contains(r#"graphql_field_execution_total{graphql_field_name="topProducts",graphql_field_type="Product",graphql_type_name="Query",otel_scope_name="apollo/router"} 1"#, None) + .await; + router + .assert_metrics_contains(r#"custom_counter_total{graphql_field_name="name",graphql_field_type="String",graphql_type_name="Product",otel_scope_name="apollo/router"} 3"#, None) + .await; + router + .assert_metrics_contains(r#"custom_histogram_sum{graphql_field_name="topProducts",graphql_field_type="Product",graphql_type_name="Query",otel_scope_name="apollo/router"} 3"#, None) + .await; +} diff --git a/apollo-router/tests/set_context.rs b/apollo-router/tests/set_context.rs new file mode 100644 index 0000000000..bc5f44d2da --- /dev/null +++ b/apollo-router/tests/set_context.rs @@ -0,0 +1,325 @@ +//! +//! Please ensure that any tests added to this file use the tokio multi-threaded test executor. +//! + +use apollo_router::graphql::Request; +use apollo_router::graphql::Response; +use apollo_router::plugin::test::MockSubgraph; +use apollo_router::services::supergraph; +use apollo_router::MockedSubgraphs; +use apollo_router::TestHarness; +use serde::Deserialize; +use serde_json::json; +use tower::ServiceExt; + +#[derive(Deserialize)] +struct SubgraphMock { + mocks: Vec, +} + +#[derive(Deserialize)] +struct RequestAndResponse { + request: Request, + response: Response, +} + +macro_rules! snap +{ + ($result:ident) => { + insta::with_settings!({sort_maps => true}, { + insta::assert_json_snapshot!($result); + }); + } +} + +async fn run_single_request(query: &str, mocks: &[(&'static str, &'static str)]) -> Response { + let harness = setup_from_mocks( + json! {{ + "experimental_type_conditioned_fetching": true, + // will make debugging easier + "plugins": { + "experimental.expose_query_plan": true + }, + "include_subgraph_errors": { + "all": true + } + }}, + mocks, + ); + let supergraph_service = harness.build_supergraph().await.unwrap(); + let request = supergraph::Request::fake_builder() + .query(query.to_string()) + .header("Apollo-Expose-Query-Plan", "true") + .variables(Default::default()) + .build() + .expect("expecting valid request"); + + supergraph_service + .oneshot(request) + .await + .unwrap() + .next_response() + .await + .unwrap() +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context() { + static QUERY: &str = r#" + query Query { + t { + __typename + id + u { + __typename + field + } + } + }"#; + + let response = run_single_request( + QUERY, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_no_typenames() { + static QUERY_NO_TYPENAMES: &str = r#" + query Query { + t { + id + u { + field + } + } + }"#; + + let response = run_single_request( + QUERY_NO_TYPENAMES, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_list() { + static QUERY_WITH_LIST: &str = r#" + query Query { + t { + id + uList { + field + } + } + }"#; + + let response = run_single_request( + QUERY_WITH_LIST, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_list_of_lists() { + static QUERY_WITH_LIST_OF_LISTS: &str = r#" + query QueryLL { + tList { + id + uList { + field + } + } + }"#; + + let response = run_single_request( + QUERY_WITH_LIST_OF_LISTS, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_union() { + static QUERY_WITH_UNION: &str = r#" + query QueryUnion { + k { + ... on A { + v { + field + } + } + ... on B { + v { + field + } + } + } + }"#; + + let response = run_single_request( + QUERY_WITH_UNION, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_with_null() { + static QUERY: &str = r#" + query Query_Null_Param { + t { + id + u { + field + } + } + }"#; + + let response = run_single_request( + QUERY, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + insta::assert_json_snapshot!(response); +} + +// this test returns the contextual value with a different than expected type +// this currently works, but perhaps should do type valdiation in the future to reject +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_type_mismatch() { + static QUERY: &str = r#" + query Query_type_mismatch { + t { + id + u { + field + } + } + }"#; + + let response = run_single_request( + QUERY, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +// fetch from unrelated (to context) subgraph fails +// validates that the error propagation is correct +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_unrelated_fetch_failure() { + static QUERY: &str = r#" + query Query_fetch_failure { + t { + id + u { + field + b + } + } + }"#; + + let response = run_single_request( + QUERY, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +// subgraph fetch fails where context depends on results of fetch. +// validates that no fetch will get called that passes context +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_dependent_fetch_failure() { + static QUERY: &str = r#" + query Query_fetch_dependent_failure { + t { + id + u { + field + } + } + }"#; + + let response = run_single_request( + QUERY, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +fn setup_from_mocks( + configuration: serde_json::Value, + mocks: &[(&'static str, &'static str)], +) -> TestHarness<'static> { + let mut mocked_subgraphs = MockedSubgraphs::default(); + + for (name, m) in mocks { + let subgraph_mock: SubgraphMock = serde_json::from_str(m).unwrap(); + + let mut builder = MockSubgraph::builder(); + + for mock in subgraph_mock.mocks { + builder = builder.with_json( + serde_json::to_value(mock.request).unwrap(), + serde_json::to_value(mock.response).unwrap(), + ); + } + + mocked_subgraphs.insert(name, builder.build()); + } + + let schema = include_str!("fixtures/set_context/supergraph.graphql"); + TestHarness::builder() + .try_log_level("info") + .configuration_json(configuration) + .unwrap() + .schema(schema) + .extra_plugin(mocked_subgraphs) +} diff --git a/apollo-router/tests/snapshots/apollo_reports__batch_send_header-2.snap b/apollo-router/tests/snapshots/apollo_reports__batch_send_header-2.snap index fb800a2d1e..15c2327c19 100644 --- a/apollo-router/tests/snapshots/apollo_reports__batch_send_header-2.snap +++ b/apollo-router/tests/snapshots/apollo_reports__batch_send_header-2.snap @@ -1100,3 +1100,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__batch_send_header.snap b/apollo-router/tests/snapshots/apollo_reports__batch_send_header.snap index fb800a2d1e..15c2327c19 100644 --- a/apollo-router/tests/snapshots/apollo_reports__batch_send_header.snap +++ b/apollo-router/tests/snapshots/apollo_reports__batch_send_header.snap @@ -1100,3 +1100,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__batch_trace_id-2.snap b/apollo-router/tests/snapshots/apollo_reports__batch_trace_id-2.snap index f79e977f99..e152be5676 100644 --- a/apollo-router/tests/snapshots/apollo_reports__batch_trace_id-2.snap +++ b/apollo-router/tests/snapshots/apollo_reports__batch_trace_id-2.snap @@ -1094,3 +1094,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__batch_trace_id.snap b/apollo-router/tests/snapshots/apollo_reports__batch_trace_id.snap index f79e977f99..e152be5676 100644 --- a/apollo-router/tests/snapshots/apollo_reports__batch_trace_id.snap +++ b/apollo-router/tests/snapshots/apollo_reports__batch_trace_id.snap @@ -1094,3 +1094,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__client_name-2.snap b/apollo-router/tests/snapshots/apollo_reports__client_name-2.snap index b1461034c3..cdd69f128b 100644 --- a/apollo-router/tests/snapshots/apollo_reports__client_name-2.snap +++ b/apollo-router/tests/snapshots/apollo_reports__client_name-2.snap @@ -557,3 +557,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__client_name.snap b/apollo-router/tests/snapshots/apollo_reports__client_name.snap index b1461034c3..cdd69f128b 100644 --- a/apollo-router/tests/snapshots/apollo_reports__client_name.snap +++ b/apollo-router/tests/snapshots/apollo_reports__client_name.snap @@ -557,3 +557,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__client_version-2.snap b/apollo-router/tests/snapshots/apollo_reports__client_version-2.snap index 5fc4275f42..3e0f375077 100644 --- a/apollo-router/tests/snapshots/apollo_reports__client_version-2.snap +++ b/apollo-router/tests/snapshots/apollo_reports__client_version-2.snap @@ -557,3 +557,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__client_version.snap b/apollo-router/tests/snapshots/apollo_reports__client_version.snap index 5fc4275f42..3e0f375077 100644 --- a/apollo-router/tests/snapshots/apollo_reports__client_version.snap +++ b/apollo-router/tests/snapshots/apollo_reports__client_version.snap @@ -557,3 +557,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__condition_else-2.snap b/apollo-router/tests/snapshots/apollo_reports__condition_else-2.snap index 4d5c99b83d..bb048564c8 100644 --- a/apollo-router/tests/snapshots/apollo_reports__condition_else-2.snap +++ b/apollo-router/tests/snapshots/apollo_reports__condition_else-2.snap @@ -563,3 +563,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__condition_else.snap b/apollo-router/tests/snapshots/apollo_reports__condition_else.snap index 4d5c99b83d..bb048564c8 100644 --- a/apollo-router/tests/snapshots/apollo_reports__condition_else.snap +++ b/apollo-router/tests/snapshots/apollo_reports__condition_else.snap @@ -563,3 +563,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__condition_if-2.snap b/apollo-router/tests/snapshots/apollo_reports__condition_if-2.snap index ad368c8265..2e45b53a67 100644 --- a/apollo-router/tests/snapshots/apollo_reports__condition_if-2.snap +++ b/apollo-router/tests/snapshots/apollo_reports__condition_if-2.snap @@ -576,3 +576,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__condition_if.snap b/apollo-router/tests/snapshots/apollo_reports__condition_if.snap index ad368c8265..2e45b53a67 100644 --- a/apollo-router/tests/snapshots/apollo_reports__condition_if.snap +++ b/apollo-router/tests/snapshots/apollo_reports__condition_if.snap @@ -576,3 +576,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__non_defer-2.snap b/apollo-router/tests/snapshots/apollo_reports__non_defer-2.snap index 3cbf105e67..16c6d72247 100644 --- a/apollo-router/tests/snapshots/apollo_reports__non_defer-2.snap +++ b/apollo-router/tests/snapshots/apollo_reports__non_defer-2.snap @@ -557,3 +557,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__non_defer.snap b/apollo-router/tests/snapshots/apollo_reports__non_defer.snap index 3cbf105e67..16c6d72247 100644 --- a/apollo-router/tests/snapshots/apollo_reports__non_defer.snap +++ b/apollo-router/tests/snapshots/apollo_reports__non_defer.snap @@ -557,3 +557,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__send_header-2.snap b/apollo-router/tests/snapshots/apollo_reports__send_header-2.snap index aec0e1e443..d4cc1e50f9 100644 --- a/apollo-router/tests/snapshots/apollo_reports__send_header-2.snap +++ b/apollo-router/tests/snapshots/apollo_reports__send_header-2.snap @@ -560,3 +560,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__send_header.snap b/apollo-router/tests/snapshots/apollo_reports__send_header.snap index aec0e1e443..d4cc1e50f9 100644 --- a/apollo-router/tests/snapshots/apollo_reports__send_header.snap +++ b/apollo-router/tests/snapshots/apollo_reports__send_header.snap @@ -560,3 +560,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__send_variable_value-2.snap b/apollo-router/tests/snapshots/apollo_reports__send_variable_value-2.snap index 91ebf06aa3..9bd1bcf5c4 100644 --- a/apollo-router/tests/snapshots/apollo_reports__send_variable_value-2.snap +++ b/apollo-router/tests/snapshots/apollo_reports__send_variable_value-2.snap @@ -559,3 +559,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__send_variable_value.snap b/apollo-router/tests/snapshots/apollo_reports__send_variable_value.snap index 91ebf06aa3..9bd1bcf5c4 100644 --- a/apollo-router/tests/snapshots/apollo_reports__send_variable_value.snap +++ b/apollo-router/tests/snapshots/apollo_reports__send_variable_value.snap @@ -559,3 +559,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__stats.snap b/apollo-router/tests/snapshots/apollo_reports__stats.snap index 5a593e3de8..ae3a3b3a16 100644 --- a/apollo-router/tests/snapshots/apollo_reports__stats.snap +++ b/apollo-router/tests/snapshots/apollo_reports__stats.snap @@ -102,6 +102,7 @@ traces_per_query: estimated_execution_count: 2 requests_with_errors_count: 0 latency_count: "[latency_count]" + extended_references: ~ local_per_type_stat: {} limits_stats: ~ operation_count: 0 @@ -131,3 +132,4 @@ operation_count_by_type: subtype: "" operation_count: 1 traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__stats_mocked.snap b/apollo-router/tests/snapshots/apollo_reports__stats_mocked.snap index bf06893e8c..d2a16ba9bc 100644 --- a/apollo-router/tests/snapshots/apollo_reports__stats_mocked.snap +++ b/apollo-router/tests/snapshots/apollo_reports__stats_mocked.snap @@ -441,6 +441,7 @@ per_type_stat: - 0 - 0 - 1 +extended_references: ~ local_per_type_stat: {} limits_stats: ~ operation_count: 0 diff --git a/apollo-router/tests/snapshots/apollo_reports__trace_id-2.snap b/apollo-router/tests/snapshots/apollo_reports__trace_id-2.snap index 3cbf105e67..16c6d72247 100644 --- a/apollo-router/tests/snapshots/apollo_reports__trace_id-2.snap +++ b/apollo-router/tests/snapshots/apollo_reports__trace_id-2.snap @@ -557,3 +557,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/apollo_reports__trace_id.snap b/apollo-router/tests/snapshots/apollo_reports__trace_id.snap index 3cbf105e67..16c6d72247 100644 --- a/apollo-router/tests/snapshots/apollo_reports__trace_id.snap +++ b/apollo-router/tests/snapshots/apollo_reports__trace_id.snap @@ -557,3 +557,4 @@ end_time: "[end_time]" operation_count: 0 operation_count_by_type: [] traces_pre_aggregated: true +extended_references_enabled: false diff --git a/apollo-router/tests/snapshots/set_context__set_context.snap b/apollo-router/tests/snapshots/set_context__set_context.snap new file mode 100644 index 0000000000..2e11680753 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context.snap @@ -0,0 +1,101 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "__typename": "T", + "id": "1", + "u": { + "__typename": "U", + "field": 1234 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationKind": "query", + "operationName": "Query__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "d7cb2d1809789d49360ca0a60570555f83855f00547675f366915c9d9d90fef9", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "Query__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "66b954f39aead8436321c671eb71e56ce15bbe0c7b82f06b2f8f70473ce1cb6e", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "t", + "u" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap new file mode 100644 index 0000000000..703d8f9c59 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap @@ -0,0 +1,98 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": null, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_dependent_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationKind": "query", + "operationName": "Query_fetch_dependent_failure__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "595c36c322602fefc4658fc0070973b51800c2d2debafae5571a7c9811d80745", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_dependent_failure__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "Query_fetch_dependent_failure__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "37bef7ad43bb477cdec4dfc02446bd2e11a6919dc14ab90e266af85fefde4abd", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "t", + "u" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + }, + "valueCompletion": [ + { + "message": "Cannot return null for non-nullable field Query.t", + "path": [] + } + ] + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_list.snap b/apollo-router/tests/snapshots/set_context__set_context_list.snap new file mode 100644 index 0000000000..095326167e --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_list.snap @@ -0,0 +1,108 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "id": "1", + "uList": [ + { + "field": 1234 + }, + { + "field": 2345 + }, + { + "field": 3456 + } + ] + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query__Subgraph1__0{t{__typename prop id uList{__typename id}}}", + "operationKind": "query", + "operationName": "Query__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "4f746b9319e3ca4f234269464b6815eb97782f2ffe36774b998e7fb78f30abef", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "Query__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "66b954f39aead8436321c671eb71e56ce15bbe0c7b82f06b2f8f70473ce1cb6e", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "t", + "uList", + "@" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n uList {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".t.uList.@\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_list_of_lists.snap b/apollo-router/tests/snapshots/set_context__set_context_list_of_lists.snap new file mode 100644 index 0000000000..e7fbee2a8b --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_list_of_lists.snap @@ -0,0 +1,113 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "tList": [ + { + "id": "1", + "uList": [ + { + "field": 3456 + } + ] + }, + { + "id": "2", + "uList": [ + { + "field": 4567 + } + ] + } + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryLL__Subgraph1__0{tList{__typename prop id uList{__typename id}}}", + "operationKind": "query", + "operationName": "QueryLL__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "babf88ea82c1330e535966572a55b03a2934097cd1cf905303b86ae7c197ccaf", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryLL__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "QueryLL__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "a9b24549250c12e38c398c32e9218134fab000be3b934ebc6bb38ea096343646", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "tList", + "@", + "uList", + "@" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n tList {\n __typename\n prop\n id\n uList {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".tList.@.uList.@\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_no_typenames.snap b/apollo-router/tests/snapshots/set_context__set_context_no_typenames.snap new file mode 100644 index 0000000000..8eaa5b0202 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_no_typenames.snap @@ -0,0 +1,99 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "id": "1", + "u": { + "field": 1234 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationKind": "query", + "operationName": "Query__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "d7cb2d1809789d49360ca0a60570555f83855f00547675f366915c9d9d90fef9", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "Query__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "66b954f39aead8436321c671eb71e56ce15bbe0c7b82f06b2f8f70473ce1cb6e", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "t", + "u" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_type_mismatch.snap b/apollo-router/tests/snapshots/set_context__set_context_type_mismatch.snap new file mode 100644 index 0000000000..1df052723e --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_type_mismatch.snap @@ -0,0 +1,99 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "id": "1", + "u": { + "field": 1234 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_type_mismatch__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationKind": "query", + "operationName": "Query_type_mismatch__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "7eae890e61f5ae512e112f5260abe0de3504041c92dbcc7aae0891c9bdf2222b", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_type_mismatch__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "Query_type_mismatch__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "d8ea99348ab32931371c85c09565cfb728d2e48cf017201cd79cb9ef860eb9c2", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "t", + "u" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_union.snap b/apollo-router/tests/snapshots/set_context__set_context_union.snap new file mode 100644 index 0000000000..e382988a8b --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_union.snap @@ -0,0 +1,157 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "k": { + "v": { + "field": 3456 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryUnion__Subgraph1__0{k{__typename ...on A{__typename prop v{__typename id}}...on B{__typename prop v{__typename id}}}}", + "operationKind": "query", + "operationName": "QueryUnion__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "b9124cd1daa6e8347175ffe2108670a31c73cbc983e7812ee39f415235541005", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on A", + "prop" + ], + "renameKeyTo": "contextualArgument_1_1" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryUnion__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_1:String){_entities(representations:$representations){...on V{field(a:$contextualArgument_1_1)}}}", + "operationKind": "query", + "operationName": "QueryUnion__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "V" + } + ], + "schemaAwareHash": "c50ca82d402a330c1b35a6d76332094c40b00d6dec6f6b2a9b0a32ced68f4e95", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_1" + ] + }, + "path": [ + "", + "k|[A]", + "v" + ] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on B", + "prop" + ], + "renameKeyTo": "contextualArgument_1_1" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryUnion__Subgraph1__2($representations:[_Any!]!$contextualArgument_1_1:String){_entities(representations:$representations){...on V{field(a:$contextualArgument_1_1)}}}", + "operationKind": "query", + "operationName": "QueryUnion__Subgraph1__2", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "V" + } + ], + "schemaAwareHash": "ec99886497fee9b4f13565e19cadb13ae85c83de93acb53f298944b7a29e630e", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_1" + ] + }, + "path": [ + "", + "k|[B]", + "v" + ] + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n k {\n __typename\n ... on A {\n __typename\n prop\n v {\n __typename\n id\n }\n }\n ... on B {\n __typename\n prop\n v {\n __typename\n id\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \".k|[A].v\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on V {\n __typename\n id\n }\n } =>\n {\n ... on V {\n field(a: $contextualArgument_1_1)\n }\n }\n },\n },\n Flatten(path: \".k|[B].v\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on V {\n __typename\n id\n }\n } =>\n {\n ... on V {\n field(a: $contextualArgument_1_1)\n }\n }\n },\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap new file mode 100644 index 0000000000..605fd4570a --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap @@ -0,0 +1,170 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": null, + "errors": [ + { + "message": "Some error", + "path": [ + "t", + "u" + ] + } + ], + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationKind": "query", + "operationName": "Query_fetch_failure__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "1813ba1c272be0201096b4c4c963a07638e4f4b4ac1b97e0d90d634f2fcbac11", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_failure__Subgraph2__1($representations:[_Any!]!){_entities(representations:$representations){...on U{b}}}", + "operationKind": "query", + "operationName": "Query_fetch_failure__Subgraph2__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "1fdff97ad7facf07690c3e75e3dc7f1b11ff509268ef999250912a728e7a94c9", + "serviceName": "Subgraph2", + "variableUsages": [] + }, + "path": [ + "", + "t", + "u" + ] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_failure__Subgraph1__2($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "Query_fetch_failure__Subgraph1__2", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "c9c571eac5df81ff34e5e228934d029ed322640c97ab6ad061cbee3cd81040dc", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "t", + "u" + ] + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Parallel {\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph2\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n b\n }\n }\n },\n },\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n },\n}" + }, + "valueCompletion": [ + { + "message": "Cannot return null for non-nullable field U.field", + "path": [ + "t", + "u" + ] + }, + { + "message": "Cannot return null for non-nullable field T.u", + "path": [ + "t", + "u" + ] + }, + { + "message": "Cannot return null for non-nullable field T!.t", + "path": [ + "t" + ] + } + ] + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_with_null.snap b/apollo-router/tests/snapshots/set_context__set_context_with_null.snap new file mode 100644 index 0000000000..1e361f0a83 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_with_null.snap @@ -0,0 +1,99 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "id": "1", + "u": { + "field": 1234 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "Subgraph1", + "variableUsages": [], + "operation": "query Query_Null_Param__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_Null_Param__Subgraph1__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "19bd66a3ecc2d9495dffce2279774de3275cb027254289bb61b0c1937a7738b4", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Flatten", + "path": [ + "", + "t", + "u" + ], + "node": { + "kind": "Fetch", + "serviceName": "Subgraph1", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "U", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "contextualArgument_1_0" + ], + "operation": "query Query_Null_Param__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query_Null_Param__Subgraph1__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "schemaAwareHash": "010ba25ca76f881bd9f0d5e338f9c07829d4d00e183828b6577d593aea0cf21e", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap b/apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap index e1b3c3bba7..84b137aa01 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap +++ b/apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap @@ -78,6 +78,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "0144f144d271437ed45f9d20706be86ffbf1e124d77c7add3db17d4a1498ce97", "authorization": { "is_authenticated": false, @@ -135,6 +136,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "23759b36e5149924c757a8b9586adec2c0f6be04ecdf2c3c3ea277446daa690b", "authorization": { "is_authenticated": false, diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap index 49e1fef4bc..e41aeefee5 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap +++ b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap @@ -78,6 +78,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "0144f144d271437ed45f9d20706be86ffbf1e124d77c7add3db17d4a1498ce97", "authorization": { "is_authenticated": false, @@ -139,6 +140,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "23759b36e5149924c757a8b9586adec2c0f6be04ecdf2c3c3ea277446daa690b", "authorization": { "is_authenticated": false, @@ -198,6 +200,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "8ee58ad8b4823bcbda9126d2565e1cb04bf91ff250b1098476a1d7614a870121", "authorization": { "is_authenticated": false, diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap index b04c2208ec..d92517b39d 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap +++ b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap @@ -78,6 +78,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "844dc4e409cdca1334abe37c347bd4e330123078dd7e65bda8dbb57ea5bdf59c", "authorization": { "is_authenticated": false, @@ -139,6 +140,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "ad82ce0af279c6a012d6b349ff823ba1467902223312aed1cdfc494ec3100b3e", "authorization": { "is_authenticated": false, @@ -198,6 +200,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "7c267302cf4a44a4463820237830155ab50be32c8860371d8a5c8ca905476360", "authorization": { "is_authenticated": false, diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap index 45697537d8..acffc62599 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap @@ -140,6 +140,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "1343b4972ec8be54afe990c69711ce790992a814f9654e34e2ee2b25e4097e45", "authorization": { "is_authenticated": false, @@ -202,6 +203,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "23759b36e5149924c757a8b9586adec2c0f6be04ecdf2c3c3ea277446daa690b", "authorization": { "is_authenticated": false, @@ -262,6 +264,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "8ee58ad8b4823bcbda9126d2565e1cb04bf91ff250b1098476a1d7614a870121", "authorization": { "is_authenticated": false, diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap index 528a658fe6..2b8feaafc3 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap @@ -144,6 +144,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "3698f4e74ead34f43a949e1e8459850337a1a07245f8ed627b9203904b4cfff4", "authorization": { "is_authenticated": false, @@ -207,6 +208,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "23759b36e5149924c757a8b9586adec2c0f6be04ecdf2c3c3ea277446daa690b", "authorization": { "is_authenticated": false, @@ -268,6 +270,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "8ee58ad8b4823bcbda9126d2565e1cb04bf91ff250b1098476a1d7614a870121", "authorization": { "is_authenticated": false, diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap index 38ef6bc51a..5020d447b4 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap +++ b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap @@ -53,6 +53,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "0144f144d271437ed45f9d20706be86ffbf1e124d77c7add3db17d4a1498ce97", "authorization": { "is_authenticated": false, @@ -114,6 +115,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "23759b36e5149924c757a8b9586adec2c0f6be04ecdf2c3c3ea277446daa690b", "authorization": { "is_authenticated": false, @@ -173,6 +175,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "8ee58ad8b4823bcbda9126d2565e1cb04bf91ff250b1098476a1d7614a870121", "authorization": { "is_authenticated": false, diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml index f90fff4010..64047bf2d6 100644 --- a/dockerfiles/tracing/docker-compose.datadog.yml +++ b/dockerfiles/tracing/docker-compose.datadog.yml @@ -3,7 +3,7 @@ services: apollo-router: container_name: apollo-router - image: ghcr.io/apollographql/router:v1.47.0 + image: ghcr.io/apollographql/router:v1.48.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/datadog.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml index 55605308eb..11b98eb021 100644 --- a/dockerfiles/tracing/docker-compose.jaeger.yml +++ b/dockerfiles/tracing/docker-compose.jaeger.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router #build: ./router - image: ghcr.io/apollographql/router:v1.47.0 + image: ghcr.io/apollographql/router:v1.48.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/jaeger.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index 1689a4e9d4..a0f0c530b7 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router build: ./router - image: ghcr.io/apollographql/router:v1.47.0 + image: ghcr.io/apollographql/router:v1.48.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/zipkin.router.yaml:/etc/config/configuration.yaml diff --git a/docs/shared/register-federated-cli.mdx b/docs/shared/register-federated-cli.mdx index c74b6492a7..a8ad0cdb93 100644 --- a/docs/shared/register-federated-cli.mdx +++ b/docs/shared/register-federated-cli.mdx @@ -17,7 +17,7 @@ graph LR; If you haven't yet: * Install the Rover CLI. -* Authenticate Rover with Apollo Studio. +* Authenticate Rover with GraphOS Studio. Then, **do the following for each of your subgraphs**: @@ -38,4 +38,4 @@ Then, **do the following for each of your subgraphs**: As you register your subgraph schemas, the schema registry attempts to **compose** their latest versions into a single **supergraph schema**. Whenever composition succeeds, the Apollo Router can fetch the latest supergraph schema from the registry. -You can also manually fetch your latest supergraph schema with the `rover supergraph fetch` command, or retrieve it from your graph's **Schema > SDL** tab in Apollo Studio. +You can also manually fetch your latest supergraph schema with the `rover supergraph fetch` command, or retrieve it from your graph's **Schema > SDL** tab in GraphOS Studio. diff --git a/docs/source/config.json b/docs/source/config.json index 468b9fe5e0..e176c00d83 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -7,8 +7,8 @@ "Introduction": "/", "Quickstart": "/quickstart", "Moving from @apollo/gateway": "/migrating-from-gateway", - "Federation version support": "/federation-version-support", - "Enterprise features": [ + "Federation Version Support": "/federation-version-support", + "Enterprise Features": [ "/enterprise-features", [ "enterprise" @@ -17,14 +17,14 @@ "Configuring the Router": { "Overview": "/configuration/overview", "Caching": { - "In-memory caching": "/configuration/in-memory-caching", - "Distributed caching": [ + "In-Memory Caching": "/configuration/in-memory-caching", + "Distributed Caching": [ "/configuration/distributed-caching", [ "enterprise" ] ], - "Entity caching": [ + "Entity Caching": [ "/configuration/entity-caching", [ "enterprise", @@ -35,15 +35,15 @@ "Debugging": { "Errors": "/errors", "Telemetry": "/configuration/telemetry/overview", - "Subgraph error inclusion": "/configuration/subgraph-error-inclusion" + "Subgraph Error Inclusion": "/configuration/subgraph-error-inclusion" }, "Networking": { - "Header propagation": "/configuration/header-propagation", - "Traffic shaping": "/configuration/traffic-shaping" + "Header Propagation": "/configuration/header-propagation", + "Traffic Shaping": "/configuration/traffic-shaping" }, "Security": { "CORS": "/configuration/cors", - "CSRF prevention": "/configuration/csrf", + "CSRF Prevention": "/configuration/csrf", "JWT Authentication": [ "/configuration/authn-jwt", [ @@ -57,46 +57,46 @@ ] ], "Subgraph Authentication": "/configuration/authn-subgraph", - "Operation limits": [ + "Operation Limits": [ "/configuration/operation-limits", [ "enterprise" ] ], - "Safelisting with persisted queries": [ + "Safelisting with Persisted Queries": [ "/configuration/persisted-queries", [ "enterprise" ] ], - "Privacy and data collection": "/privacy" + "Privacy and Data Collection": "/privacy" } }, "Executing Operations": { - "Build and run queries": "/executing-operations/build-run-queries", - "@defer support": "/executing-operations/defer-support", - "Request format": "/executing-operations/requests", - "Query batching": [ + "Build and Run Queries": "/executing-operations/build-run-queries", + "@defer Support": "/executing-operations/defer-support", + "Request Format": "/executing-operations/requests", + "Query Batching": [ "/executing-operations/query-batching", [ "enterprise" ] ], "GraphQL Subscriptions": { - "Subscriptions setup": [ + "Subscriptions Setup": [ "/executing-operations/subscription-support", [ "enterprise" ] ], - "Subgraph protocol: HTTP callback": [ + "Subgraph Protocol: HTTP Callback": [ "/executing-operations/subscription-callback-protocol", [ "enterprise", "preview" ] ], - "Client protocol: HTTP multipart": [ + "Client Protocol: HTTP Multipart": [ "/executing-operations/subscription-multipart-protocol", [ "enterprise" @@ -106,13 +106,13 @@ }, "Telemetry and Monitoring": { "Overview": "/configuration/telemetry/overview", - "GraphOS reporting": "/configuration/telemetry/apollo-telemetry", - "Client awareness": "/managed-federation/client-awareness", - "Log exporters": { + "GraphOS Reporting": "/configuration/telemetry/apollo-telemetry", + "Client Awareness": "/managed-federation/client-awareness", + "Log Exporters": { "Configuration": "/configuration/telemetry/exporters/logging/overview", "Stdout": "/configuration/telemetry/exporters/logging/stdout" }, - "Metrics exporters": { + "Metrics Exporters": { "Configuration": "/configuration/telemetry/exporters/metrics/overview", "Datadog": "/configuration/telemetry/exporters/metrics/datadog", "New Relic": "/configuration/telemetry/exporters/metrics/new-relic", @@ -130,39 +130,40 @@ "Instrumentation": { "Instruments": "/configuration/telemetry/instrumentation/instruments", "Events": "/configuration/telemetry/instrumentation/events", + "Conditions": "/configuration/telemetry/instrumentation/conditions", "Spans": "/configuration/telemetry/instrumentation/spans", "Selectors": "/configuration/telemetry/instrumentation/selectors", - "Standard attributes": "/configuration/telemetry/instrumentation/standard-attributes", - "Standard instruments": "/configuration/telemetry/instrumentation/standard-instruments" + "Standard Attributes": "/configuration/telemetry/instrumentation/standard-attributes", + "Standard Instruments": "/configuration/telemetry/instrumentation/standard-instruments" } }, "Containerization": { "Overview": "/containerization/overview", "Deploy on Kubernetes": "/containerization/kubernetes", "Run with Docker": "/containerization/docker", - "Health checks": "/configuration/health-checks" + "Health Checks": "/configuration/health-checks" }, "Managed Federation": { "Overview": "https://www.apollographql.com/docs/federation/managed-federation/overview", "Setup": "https://www.apollographql.com/docs/federation/managed-federation/setup", - "GraphOS Studio features": "https://www.apollographql.com/docs/graphos/graphs/federated-graphs" + "GraphOS Studio Features": "https://www.apollographql.com/docs/graphos/graphs/federated-graphs" }, "Customizations": { "Overview": "/customizations/overview", - "Rhai scripts": "/customizations/rhai", - "Rhai API reference": "/customizations/rhai-api", - "External coprocessing": [ + "Rhai Scripts": "/customizations/rhai", + "Rhai API Reference": "/customizations/rhai-api", + "External Coprocessing": [ "/customizations/coprocessor", [ "enterprise" ] ], - "Native Rust plugins": "/customizations/native", - "Custom router binary": "/customizations/custom-binary" + "Native Rust Plugins": "/customizations/native", + "Custom Router Binary": "/customizations/custom-binary" }, "Subgraph Support": { - "Subgraph-compatible libraries": "https://www.apollographql.com/docs/federation/v2/other-servers/", - "Subgraph specification": "https://www.apollographql.com/docs/federation/v2/federation-spec/" + "Subgraph-Compatible Libraries": "https://www.apollographql.com/docs/federation/v2/other-servers/", + "Subgraph Specification": "https://www.apollographql.com/docs/federation/v2/federation-spec/" } } } diff --git a/docs/source/configuration/authn-jwt.mdx b/docs/source/configuration/authn-jwt.mdx index f99c6e489b..3affa08fa9 100644 --- a/docs/source/configuration/authn-jwt.mdx +++ b/docs/source/configuration/authn-jwt.mdx @@ -1,6 +1,7 @@ --- title: JWT Authentication in the Apollo Router -description: Restrict access to credentialed users and systems +subtitle: Restrict access to credentialed users and systems +description: Protect sensitive data by enabling JWT authentication in the Apollo Router. Restrict access to credentialed users and systems. --- diff --git a/docs/source/configuration/authn-subgraph.mdx b/docs/source/configuration/authn-subgraph.mdx index 771af8ac1e..c9f1a3c378 100644 --- a/docs/source/configuration/authn-subgraph.mdx +++ b/docs/source/configuration/authn-subgraph.mdx @@ -1,5 +1,7 @@ --- title: Subgraph Authentication in the Apollo Router +subtitle: Implement subgraph authentication using AWS SigV4 +description: Secure communication to AWS subgraphs via the Apollo Router using AWS Signature Version 4 (SigV4). minVersion: 1.27.0 --- diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index e1e845ad14..3566babead 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -1,6 +1,7 @@ --- title: Authorization in the Apollo Router -description: Strengthen service security with a centralized governance layer +subtitle: Strengthen subgraph security with a centralized governance layer +description: Enforce authorization in the Apollo Router with the @requireScopes, @authenticated, and @policy directives. --- diff --git a/docs/source/configuration/cors.mdx b/docs/source/configuration/cors.mdx index 71ac87fd28..43ac8f20ee 100644 --- a/docs/source/configuration/cors.mdx +++ b/docs/source/configuration/cors.mdx @@ -1,7 +1,7 @@ --- title: Configuring CORS in the Apollo Router -sidebar_title: CORS -description: Control browser access to your router +subtitle: Control browser access to your router +description: Manage browser access to your Apollo Router with CORS configuration options, including origin whitelisting, wildcard origins, and credential passing. --- @@ -13,7 +13,7 @@ description: Control browser access to your router -By default, the Apollo Router enables _only_ Apollo Studio to initiate browser connections to it. If your supergraph serves data to other browser-based applications, you need to do one of the following in the `cors` section of your router's [YAML config file](./overview/#yaml-config-file): +By default, the Apollo Router enables _only_ GraphOS Studio to initiate browser connections to it. If your supergraph serves data to other browser-based applications, you need to do one of the following in the `cors` section of your router's [YAML config file](./overview/#yaml-config-file): * Add the origins of those web applications to the router's list of allowed `origins`. * Use this option if there is a known, finite list of web applications that consume your supergraph. @@ -36,13 +36,13 @@ cors: # List of accepted origins # (Ignored if allow_any_origin is true) - # (Defaults to the Apollo Studio url: `https://studio.apollographql.com`) + # (Defaults to the GraphOS Studio url: `https://studio.apollographql.com`) # # An origin is a combination of scheme, hostname and port. # It does not have any path section, so no trailing slash. origins: - https://www.your-app.example.com - - https://studio.apollographql.com # Keep this so Apollo Studio can run queries against your router + - https://studio.apollographql.com # Keep this so GraphOS Studio can run queries against your router match_origins: - "^https://([a-z0-9]+[.])*api[.]example[.]com$" # any host that uses https and ends with .api.example.com ``` @@ -110,7 +110,7 @@ cors: # An origin is a combination of scheme, hostname and port. # It does not have any path section, so no trailing slash. origins: - - https://studio.apollographql.com # Keep this so Apollo Studio can still run queries against your router + - https://studio.apollographql.com # Keep this so GraphOS Studio can still run queries against your router # Set to true to add the `Access-Control-Allow-Credentials` header allow_credentials: false diff --git a/docs/source/configuration/csrf.mdx b/docs/source/configuration/csrf.mdx index 8a05e3e3bf..4a9860e781 100644 --- a/docs/source/configuration/csrf.mdx +++ b/docs/source/configuration/csrf.mdx @@ -1,6 +1,5 @@ --- title: Cross-Site Request Forgery (CSRF) Prevention -sidebar_title: CSRF prevention subtitle: Prevent CSRF attacks in the Apollo Router description: Prevent cross-site request forgery (CSRF) attacks in the Apollo Router. minVersion: 0.9.0 diff --git a/docs/source/configuration/distributed-caching.mdx b/docs/source/configuration/distributed-caching.mdx index cc2b64ee57..3938f832e7 100644 --- a/docs/source/configuration/distributed-caching.mdx +++ b/docs/source/configuration/distributed-caching.mdx @@ -1,6 +1,6 @@ --- -title: Distributed caching for the Apollo Router -subtitle: Redis-backed caching for query plans and APQ +title: Distributed Caching for the Apollo Router +subtitle: Configure Redis-backed caching for query plans and APQ description: Distributed caching for Apollo Router with GraphOS Enterprise. Configure a Redis-backed cache for query plans and automatic persisted queries (APQ). --- diff --git a/docs/source/configuration/entity-caching.mdx b/docs/source/configuration/entity-caching.mdx index 1122ada2ae..20f83a5529 100644 --- a/docs/source/configuration/entity-caching.mdx +++ b/docs/source/configuration/entity-caching.mdx @@ -1,6 +1,6 @@ --- -title: Subgraph entity caching for the Apollo Router -subtitle: Redis-backed caching for entities +title: Subgraph Entity Caching for the Apollo Router +subtitle: Configure Redis-backed caching for entities description: Subgraph entity caching for Apollo Router with GraphOS Enterprise. Cache and reuse individual entities across queries. minVersion: 1.40.0 --- diff --git a/docs/source/configuration/header-propagation.mdx b/docs/source/configuration/header-propagation.mdx index 7078e36afa..2dfc151548 100644 --- a/docs/source/configuration/header-propagation.mdx +++ b/docs/source/configuration/header-propagation.mdx @@ -1,6 +1,7 @@ --- -title: Sending HTTP headers to subgraphs -description: Configure which headers the Apollo Router sends to which subgraphs +title: Header Propogation +subtitle: Configure HTTP header propagation to subgraphs +description: Configure which HTTP headers the Apollo Router sends to which subgraphs. Define per-subgraph header rules, along with rules that apply to all subgraphs. --- You can configure which HTTP headers the Apollo Router includes in its requests to each of your subgraphs. You can define per-subgraph header rules, along with rules that apply to _all_ subgraphs. diff --git a/docs/source/configuration/health-checks.mdx b/docs/source/configuration/health-checks.mdx index 0ef40f1a8b..8d0bfd3dba 100644 --- a/docs/source/configuration/health-checks.mdx +++ b/docs/source/configuration/health-checks.mdx @@ -1,6 +1,7 @@ --- -title: Health checks in the Apollo Router -description: Determining the router's status +title: Health Checks in the Apollo Router +subtitle: Determining the router's status +description: Learn how to run health checks to determine whether an Apollo Router is available and ready to start serving traffic. --- Health checks are often used by load balancers to determine whether a server is available and ready to start serving traffic. diff --git a/docs/source/configuration/in-memory-caching.mdx b/docs/source/configuration/in-memory-caching.mdx index ccbb10ec3d..4e483906a6 100644 --- a/docs/source/configuration/in-memory-caching.mdx +++ b/docs/source/configuration/in-memory-caching.mdx @@ -1,5 +1,7 @@ --- -title: In-memory caching in the Apollo Router +title: In-Memory Caching in the Apollo Router +subtitle: Configure caching for query plans and automatic persisted queries +description: Configure the Apollo Router's in-memory caching for improved performance. Configure query plans and automatic persisted queries caching. --- The Apollo Router uses an in-memory LRU cache to store the following data: @@ -17,6 +19,7 @@ If you have a GraphOS Enterprise plan, you can also configure a Redis-backed _di ## Performance improvements vs stability + The Router is a highly scalable and low-latency runtime. Even with all caching **disabled**, the time to process operations and query plans will be very minimal (nanoseconds to milliseconds) when compared to the overall supergraph request, except in the edge cases of extremely large operations and supergraphs. Caching offers stability to those running a large graph so that your overhead for given operations stays consistent, not that it dramatically improves. If you would like to validate the performance wins of operation caching, check out the [traces and metrics in the Router](/router/configuration/telemetry/instrumentation/standard-instruments#performance) to take measurements before and after. In extremely large edge cases though, we have seen the cache save 2-10x time to create the query plan, which is still a small part of the overall request. ## Caching query plans diff --git a/docs/source/configuration/operation-limits.mdx b/docs/source/configuration/operation-limits.mdx index 4641fe1ba0..d94a578485 100644 --- a/docs/source/configuration/operation-limits.mdx +++ b/docs/source/configuration/operation-limits.mdx @@ -1,6 +1,7 @@ --- -title: Enforcing operation limits in the Apollo Router -description: With GraphOS Enterprise +title: Enforcing Operation Limits in the Apollo Router +subtitle: Set constraints on depth, height, aliases, and root fields +description: Ensure your GraphQL operations are secure with Apollo Router's operation limits. Set constraints on depth, height, aliases, and root fields. --- diff --git a/docs/source/configuration/overview.mdx b/docs/source/configuration/overview.mdx index fd3bf99f30..f70fcaea1b 100644 --- a/docs/source/configuration/overview.mdx +++ b/docs/source/configuration/overview.mdx @@ -1,6 +1,7 @@ --- title: Configuring the Apollo Router -description: Command arguments and YAML config +subtitle: With environment variables, command-line options, and YAML file configuration +description: Learn how to configure the Apollo Router with environment variables, command-line options and commands, and YAML configuration files. --- import RedisTLS from '../../shared/redis-tls.mdx' @@ -836,6 +837,24 @@ In versions of the Apollo Router prior to 1.17, this limit was defined via the c +### Early cancel + +Up until [Apollo Router 1.43.1](https://github.com/apollographql/router/releases/tag/v1.43.1), when the client closed the connection without waiting for the response, the entire request was cancelled and did not go through the entire pipeline. Since this causes issues with request monitoring, the Router introduced a new behaviour in 1.43.1. Now, the entire pipeline is executed if the request is detected as cancelled, but subgraph requests are not actually done. The response will be reported with the `499` status code, but not actually sent to the client. +To go back to the previous behaviour of immediately cancelling the request, the following configuration can be used: + +```yaml +supergraph: + early_cancel: true +``` + +Additionally, since 1.43.1, the Apollo Router can show a log when it detects that the client canceled the request. This log can be activated with: + +```yaml title="router.yaml" +supergraph: + experimental_log_on_broken_pipe: true +``` + + ### Plugins You can customize the Apollo Router's behavior with [plugins](../customizations/overview). Each plugin can have its own section in the configuration file with arbitrary values: diff --git a/docs/source/configuration/persisted-queries.mdx b/docs/source/configuration/persisted-queries.mdx index 2566ffbbc2..ae7c098370 100644 --- a/docs/source/configuration/persisted-queries.mdx +++ b/docs/source/configuration/persisted-queries.mdx @@ -1,6 +1,7 @@ --- -title: Safelisting with persisted queries -description: Secure your graph while minimizing request latency +title: Safelisting with Persisted Queries +subtitle: Secure your graph while minimizing request latency +description: Secure your federated GraphQL API by creating an allowlist of trusted operations. Minimize request latency and enhance performance. minVersion: 1.25.0 --- diff --git a/docs/source/configuration/subgraph-error-inclusion.mdx b/docs/source/configuration/subgraph-error-inclusion.mdx index 72154feeee..a5a13c77ec 100644 --- a/docs/source/configuration/subgraph-error-inclusion.mdx +++ b/docs/source/configuration/subgraph-error-inclusion.mdx @@ -1,5 +1,7 @@ --- -title: Subgraph error inclusion +title: Subgraph Error Inclusion +subtitle: Configure the router to propagate subgraph errors to clients +description: Configure the Apollo Router to propagate subgraph errors to clients for all subgraphs or on a per-subgraph basis. --- By default, the Apollo Router redacts the details of subgraph errors in its responses to clients. The router instead returns a default error with the following message: diff --git a/docs/source/configuration/telemetry/apollo-telemetry.mdx b/docs/source/configuration/telemetry/apollo-telemetry.mdx index 50c6e63935..847aa23cd4 100644 --- a/docs/source/configuration/telemetry/apollo-telemetry.mdx +++ b/docs/source/configuration/telemetry/apollo-telemetry.mdx @@ -1,7 +1,7 @@ --- title: GraphOS reporting subtitle: Send Apollo Router operation metrics to GraphOS -description: Report operation usage metrics from the Apollo Router to GraphOS to enable schema checks and metrics visualization in GraphOS Studio +description: Report GraphQL operation usage metrics from the Apollo Router to GraphOS to enable schema checks and metrics visualization in GraphOS Studio. --- The Apollo Router can report operation usage metrics to [GraphOS](/graphos/) that you can then visualize in GraphOS Studio. These metrics also enable powerful GraphOS features like [schema checks](/graphos/delivery/schema-checks/). @@ -280,17 +280,17 @@ An array of names for the variables that the router _will not_ report to GraphOS ```yaml title="router.yaml" telemetry: apollo: - # The percentage of requests will include HTTP request and response headers in traces sent to Apollo Studio. + # The percentage of requests will include HTTP request and response headers in traces sent to GraphOS Studio. # This is expensive and should be left at a low value. # This cannot be higher than tracing->common->sampler field_level_instrumentation_sampler: 0.01 # (default) - # Include HTTP request and response headers in traces sent to Apollo Studio + # Include HTTP request and response headers in traces sent to GraphOS Studio send_headers: # other possible values are all, only (with an array), except (with an array), none (by default) except: # Send all headers except referer - referer - # Include variable values in Apollo in traces sent to Apollo Studio + # Include variable values in Apollo in traces sent to GraphOS Studio send_variable_values: # other possible values are all, only (with an array), except (with an array), none (by default) except: # Send all variable values except for variable named first - first @@ -331,3 +331,10 @@ telemetry: account: # Override the default behavior for the "account" subgraph 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. + + \ No newline at end of file diff --git a/docs/source/configuration/telemetry/exporters/metrics/datadog.mdx b/docs/source/configuration/telemetry/exporters/metrics/datadog.mdx index cda1177868..bcf4ddb6ea 100644 --- a/docs/source/configuration/telemetry/exporters/metrics/datadog.mdx +++ b/docs/source/configuration/telemetry/exporters/metrics/datadog.mdx @@ -1,7 +1,7 @@ --- title: Datadog exporter (via OTLP) subtitle: Configure the Datadog exporter for metrics -description: Configure the Datadog exporter for metrics via OpenTelemetry Protocol (OTLP) in the Apollo Router +description: Configure the Datadog exporter for metrics via OpenTelemetry Protocol (OTLP) in the Apollo Router. --- Enable and configure the [OTLP exporter](./otlp) for metrics in the Apollo Router for use with [Datadog](https://www.datadoghq.com/). diff --git a/docs/source/configuration/telemetry/exporters/metrics/new-relic.mdx b/docs/source/configuration/telemetry/exporters/metrics/new-relic.mdx index 2767f5d4f8..13402a1ed8 100644 --- a/docs/source/configuration/telemetry/exporters/metrics/new-relic.mdx +++ b/docs/source/configuration/telemetry/exporters/metrics/new-relic.mdx @@ -1,7 +1,7 @@ --- title: New Relic exporter (via OTLP) subtitle: Configure the New Relic exporter for metrics -description: Configure the New Relic exporter for metrics via OpenTelemetry Protocol (OTLP) in the Apollo Router +description: Configure the New Relic exporter for metrics via OpenTelemetry Protocol (OTLP) in the Apollo Router. --- Enable and configure the [OTLP exporter](./otlp) for metrics in the Apollo Router for use with [New Relic](https://newrelic.com/). diff --git a/docs/source/configuration/telemetry/exporters/metrics/otlp.mdx b/docs/source/configuration/telemetry/exporters/metrics/otlp.mdx index 3bf95ca582..8a213b694a 100644 --- a/docs/source/configuration/telemetry/exporters/metrics/otlp.mdx +++ b/docs/source/configuration/telemetry/exporters/metrics/otlp.mdx @@ -1,7 +1,7 @@ --- title: OpenTelemetry Protocol (OTLP) exporter subtitle: Configure the OpenTelemetry Protocol (OTLP) exporter for metrics -description: Configure the OpenTelemetry Protocol (OTLP) exporter for metrics in the Apollo Router +description: Configure the OpenTelemetry Protocol (OTLP) exporter for metrics in the Apollo Router. --- import BatchProcessorPreamble from '../../../../../shared/batch-processor-preamble.mdx'; import BatchProcessorRef from '../../../../../shared/batch-processor-ref.mdx'; diff --git a/docs/source/configuration/telemetry/exporters/metrics/overview.mdx b/docs/source/configuration/telemetry/exporters/metrics/overview.mdx index 12f105df30..c4414d7fa4 100644 --- a/docs/source/configuration/telemetry/exporters/metrics/overview.mdx +++ b/docs/source/configuration/telemetry/exporters/metrics/overview.mdx @@ -1,7 +1,7 @@ --- title: Metrics exporters subtitle: Export Apollo Router metrics -description: Collect and export metrics from the Apollo Router for Prometheus, OpenTelemetry Protocol (OTLP), Datadog, and New Relic +description: Collect and export metrics from the Apollo Router for Prometheus, OpenTelemetry Protocol (OTLP), Datadog, and New Relic. --- The Apollo Router supports collection of metrics with [OpenTelemetry](https://opentelemetry.io/), with exporters for: diff --git a/docs/source/configuration/telemetry/exporters/metrics/prometheus.mdx b/docs/source/configuration/telemetry/exporters/metrics/prometheus.mdx index e22482dcd3..f958931d14 100644 --- a/docs/source/configuration/telemetry/exporters/metrics/prometheus.mdx +++ b/docs/source/configuration/telemetry/exporters/metrics/prometheus.mdx @@ -1,7 +1,7 @@ --- title: Prometheus exporter subtitle: Configure the Prometheus metrics exporter -description: Configure the Prometheus metrics exporter endpoint in the Apollo Router +description: Configure the Prometheus metrics exporter endpoint in the Apollo Router. --- Enable and configure the [Prometheus](https://www.prometheus.io/) exporter for metrics in the Apollo Router. diff --git a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx index b889e42e3c..c8a491e760 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx @@ -1,7 +1,7 @@ --- title: Datadog exporter (via OTLP) subtitle: Configure the Datadog exporter for tracing -description: Configure the Datadog exporter for tracing via OpenTelemetry Protocol (OTLP) in the Apollo Router +description: Configure the Datadog exporter for tracing via OpenTelemetry Protocol (OTLP) in the Apollo Router. --- import BatchProcessorPreamble from '../../../../../shared/batch-processor-preamble.mdx'; import BatchProcessorRef from '../../../../../shared/batch-processor-ref.mdx'; diff --git a/docs/source/configuration/telemetry/exporters/tracing/jaeger.mdx b/docs/source/configuration/telemetry/exporters/tracing/jaeger.mdx index 978ebbf0fe..12e5c17739 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/jaeger.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/jaeger.mdx @@ -1,7 +1,7 @@ --- title: Jaeger exporter (via OTLP) subtitle: Configure the Jaeger exporter for tracing -description: Configure the Jaeger exporter for tracing via OpenTelemetry Protocol (OTLP) in the Apollo Router +description: Configure the Jaeger exporter for tracing via OpenTelemetry Protocol (OTLP) in the Apollo Router. --- import BatchProcessorPreamble from '../../../../../shared/batch-processor-preamble.mdx'; diff --git a/docs/source/configuration/telemetry/exporters/tracing/new-relic.mdx b/docs/source/configuration/telemetry/exporters/tracing/new-relic.mdx index cb1775d71a..a730e4f191 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/new-relic.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/new-relic.mdx @@ -1,7 +1,7 @@ --- title: New Relic exporter (via OTLP) subtitle: Configure the New Relic exporter for tracing -description: Configure the New Relic exporter for tracing via OpenTelemetry Protocol (OTLP) in the Apollo Router +description: Configure the New Relic exporter for tracing via OpenTelemetry Protocol (OTLP) in the Apollo Router. --- Enable and configure the [OTLP exporter](./otlp) for tracing in the Apollo Router for use with [New Relic](https://newrelic.com/). diff --git a/docs/source/configuration/telemetry/exporters/tracing/otlp.mdx b/docs/source/configuration/telemetry/exporters/tracing/otlp.mdx index 23888186d1..e2d7888872 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/otlp.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/otlp.mdx @@ -1,7 +1,7 @@ --- title: OpenTelemetry Protocol (OTLP) exporter subtitle: Configure the OpenTelemetry Protocol exporter for tracing -description: Configure the OpenTelemetry Protocol (OTLP) exporter for tracing in the Apollo Router +description: Configure the OpenTelemetry Protocol (OTLP) exporter for tracing in the Apollo Router. --- import BatchProcessorPreamble from '../../../../../shared/batch-processor-preamble.mdx'; import BatchProcessorRef from '../../../../../shared/batch-processor-ref.mdx'; diff --git a/docs/source/configuration/telemetry/exporters/tracing/zipkin.mdx b/docs/source/configuration/telemetry/exporters/tracing/zipkin.mdx index 1901d02382..f534c940a4 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/zipkin.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/zipkin.mdx @@ -1,7 +1,7 @@ --- title: Zipkin exporter subtitle: Configure the Zipkin exporter for tracing -description: Enable and configure the Zipkin exporter for tracing in the Apollo Router +description: Enable and configure the Zipkin exporter for tracing in the Apollo Router. --- import BatchProcessorPreamble from '../../../../../shared/batch-processor-preamble.mdx'; import BatchProcessorRef from '../../../../../shared/batch-processor-ref.mdx'; diff --git a/docs/source/configuration/telemetry/instrumentation/conditions.mdx b/docs/source/configuration/telemetry/instrumentation/conditions.mdx index 9b8c569dbf..238fe4db0b 100644 --- a/docs/source/configuration/telemetry/instrumentation/conditions.mdx +++ b/docs/source/configuration/telemetry/instrumentation/conditions.mdx @@ -1,11 +1,9 @@ --- title: Conditions subtitle: Set conditions for when events or instruments are triggered -description: Set conditions for when events or instruments are triggered in the Apollo Router +description: Set conditions for when events or instruments are triggered in the Apollo Router. --- - - You can set conditions for when an [instrument](./instruments) should be mutated or an [event](./events) should be triggered. diff --git a/docs/source/configuration/telemetry/instrumentation/selectors.mdx b/docs/source/configuration/telemetry/instrumentation/selectors.mdx index 5a6db45666..303679dc4d 100644 --- a/docs/source/configuration/telemetry/instrumentation/selectors.mdx +++ b/docs/source/configuration/telemetry/instrumentation/selectors.mdx @@ -30,18 +30,18 @@ Each service of the router pipeline (`router`, `supergraph`, `subgraph`) has its The router service is the initial entrypoint for all requests. It is HTTP centric and deals with opaque bytes. -| Selector | Defaultable | Values | Description | -|--------------------|-------------|-----------------------------|--------------------------------------| -| `trace_id` | Yes | `open_telemetry`\|`datadog` | The trace ID | -| `request_header` | Yes | | The name of the request header | -| `response_header` | Yes | | The name of a response header | -| `response_status` | Yes | `code`\|`reason` | The response status | -| `response_context` | Yes | | The name of a response context key | -| `baggage` | Yes | | The name of a baggage item | -| `env` | Yes | | The name of an environment variable | -| `on_graphql_error` | No | `true`|`false` | Boolean set to true if the response payload contains a graphql error | -| `static` | No | | A static string value | -| `error` | No | `reason` | a string value containing error reason when it's a critical error | +| Selector | Defaultable | Values | Description | +|--------------------|-------------|-------------------------------------------|--------------------------------------| +| `trace_id` | Yes | `open_telemetry`\|`datadog`\|`apollo` | The trace ID | +| `request_header` | Yes | | The name of the request header | +| `response_header` | Yes | | The name of a response header | +| `response_status` | Yes | `code`\|`reason` | The response status | +| `response_context` | Yes | | The name of a response context key | +| `baggage` | Yes | | The name of a baggage item | +| `env` | Yes | | The name of an environment variable | +| `on_graphql_error` | No | `true`|`false` | Boolean set to true if the response payload contains a graphql error | +| `static` | No | | A static string value | +| `error` | No | `reason` | a string value containing error reason when it's a critical error | #### Supergraph @@ -59,6 +59,7 @@ The supergraph service is executed after query parsing but before query executio | `response_errors` | Yes | | Json Path into the supergraph response body errors (it might impact performances) | | `request_context` | Yes | | The name of a request context key | | `response_context` | Yes | | The name of a response context key | +| `on_graphql_error` | No | `true`|`false` | Boolean set to true if the response payload contains a graphql error | | `baggage` | Yes | | The name of a baggage item | | `env` | Yes | | The name of an environment variable | | `static` | No | | A static string value | diff --git a/docs/source/configuration/telemetry/instrumentation/spans.mdx b/docs/source/configuration/telemetry/instrumentation/spans.mdx index 10776c8aba..a58c8bd472 100644 --- a/docs/source/configuration/telemetry/instrumentation/spans.mdx +++ b/docs/source/configuration/telemetry/instrumentation/spans.mdx @@ -152,6 +152,34 @@ The `mode` option will be defaulted to `spec_compliant` in a future release, and +## Span status + +By default spans are marked in error only if the http status code is different than 200. If you want to mark a span in error for other reason you can override the `otel.status_code` attribute which is responsible to mark a span in error or not. +If it's in error then `otel.status_code` = `error`, if not it will be `ok`. + +Here is an example if you want to mark the `router` and `supergraph` span in error if you have a graphql error in the payload. + +```yaml title="router.yaml" +telemetry: + instrumentation: + spans: + router: + attributes: + otel.status_code: + static: error + condition: + eq: + - true + - on_graphql_error: true + supergraph: + attributes: + otel.status_code: + static: error + condition: + exists: + response_errors: $[0].extensions.code # Here is an example to get the first error code, `on_graphql_error` is also available for supergraph +``` + ## Span configuration example An example configuration of `telemetry.spans` in `router.yaml` sets both standard and custom attributes for the router service: @@ -191,7 +219,8 @@ telemetry: default: "unknown" "acme.custom_3": env: "ENV_VAR" - "static_attribute": "my_static_value" + "static_attribute": + static: "my_static_value" # ... supergraph: diff --git a/docs/source/configuration/telemetry/instrumentation/standard-attributes.mdx b/docs/source/configuration/telemetry/instrumentation/standard-attributes.mdx index 2961909ba0..b96bdcb877 100644 --- a/docs/source/configuration/telemetry/instrumentation/standard-attributes.mdx +++ b/docs/source/configuration/telemetry/instrumentation/standard-attributes.mdx @@ -1,7 +1,7 @@ --- title: OpenTelemetry standard attributes subtitle: Attach standard attributes to router telemetry -description: Attach OpenTelemetry standard attributes to Apollo Router telemetry +description: Attach OpenTelemetry (OTel) standard attributes to Apollo Router telemetry. --- import RouterServices from '../../../../shared/router-lifecycle-services.mdx'; diff --git a/docs/source/configuration/telemetry/overview.mdx b/docs/source/configuration/telemetry/overview.mdx index 86513584b7..fe53b02b31 100644 --- a/docs/source/configuration/telemetry/overview.mdx +++ b/docs/source/configuration/telemetry/overview.mdx @@ -1,7 +1,7 @@ --- title: Apollo Router Telemetry subtitle: Collect observable data to monitor your router and supergraph -description: Observe and monitor the health and performance of the Apollo Router and the supergraph by collecting and exporting telemetry logs, metrics, and traces +description: Observe and monitor the health and performance of GraphQL operations in the Apollo Router by collecting and exporting telemetry logs, metrics, and traces. --- import TelemetryPerformanceNote from '../../../shared/telemetry-performance.mdx'; diff --git a/docs/source/configuration/traffic-shaping.mdx b/docs/source/configuration/traffic-shaping.mdx index 573dff2209..da93735410 100644 --- a/docs/source/configuration/traffic-shaping.mdx +++ b/docs/source/configuration/traffic-shaping.mdx @@ -1,7 +1,7 @@ --- -title: Traffic shaping in the Apollo Router -subtitle: ' ' -description: "Tune the performance and reliability of traffic to and from the Apollo Router." +title: Traffic Shaping in the Apollo Router +subtitle: Tune the performance and reliability of traffic to and from the router +description: Fine-tune traffic performance and reliability in Apollo Router with YAML configuration for client and subgraph traffic shaping. --- The Apollo Router provides various features to improve the performance and reliability of diff --git a/docs/source/containerization/docker.mdx b/docs/source/containerization/docker.mdx index 20ea2aab2a..f14037a4df 100644 --- a/docs/source/containerization/docker.mdx +++ b/docs/source/containerization/docker.mdx @@ -1,6 +1,7 @@ --- title: Run in Docker -description: Run the Apollo Router container image in Docker +subtitle: Run the Apollo Router container image in Docker +description: Run the Apollo Router container image in Docker with examples covering basic setup, configuration overrides, debugging, and building custom Docker images. --- import { Link } from "gatsby"; diff --git a/docs/source/containerization/kubernetes.mdx b/docs/source/containerization/kubernetes.mdx index 849f4a6221..8e6d483559 100644 --- a/docs/source/containerization/kubernetes.mdx +++ b/docs/source/containerization/kubernetes.mdx @@ -1,6 +1,7 @@ --- title: Deploy in Kubernetes -description: Self-hosted deployment of Apollo Router in Kubernetes +subtitle: Self-hosted deployment of Apollo Router in Kubernetes +description: Deploy the Apollo Router in Kubernetes using Helm charts. Customize configurations, enable metrics, and choose values for migration. --- import { Link } from 'gatsby'; diff --git a/docs/source/containerization/overview.mdx b/docs/source/containerization/overview.mdx index b84df29318..1dd649fa7d 100644 --- a/docs/source/containerization/overview.mdx +++ b/docs/source/containerization/overview.mdx @@ -1,6 +1,7 @@ --- title: Containerizing the Apollo Router -description: Run Apollo Router images in containers +subtitle: Run Apollo Router images in containers +description: Containerize the Apollo Router for portability and scalability. Choose from default or debug images. Deploy in Kubernetes or run in Docker. --- import ElasticNotice from '../../shared/elastic-notice.mdx'; diff --git a/docs/source/customizations/coprocessor.mdx b/docs/source/customizations/coprocessor.mdx index 5ae482d8ef..e41f4df2ca 100644 --- a/docs/source/customizations/coprocessor.mdx +++ b/docs/source/customizations/coprocessor.mdx @@ -1,6 +1,7 @@ --- -title: External coprocessing in the Apollo Router -description: Customize your router's behavior in any language +title: External Coprocessing in the Apollo Router +subtitle: Customize your router's behavior in any language +description: Customize the Apollo 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'; diff --git a/docs/source/customizations/custom-binary.mdx b/docs/source/customizations/custom-binary.mdx index 0ef949bba9..4af39b9281 100644 --- a/docs/source/customizations/custom-binary.mdx +++ b/docs/source/customizations/custom-binary.mdx @@ -1,6 +1,7 @@ --- -title: Creating a custom Apollo Router binary -description: Compile a custom router binary from source +title: Creating a Custom Apollo Router Binary +subtitle: Compile a custom router binary from source +description: Compile a custom Apollo Router binary from source. Learn to create native Rust plugins. --- import ElasticNotice from '../../shared/elastic-notice.mdx'; diff --git a/docs/source/customizations/native.mdx b/docs/source/customizations/native.mdx index 616e051cf4..b297cb1670 100644 --- a/docs/source/customizations/native.mdx +++ b/docs/source/customizations/native.mdx @@ -1,6 +1,7 @@ --- -title: Writing native Rust plugins for the Apollo Router -description: Extend the Apollo Router with custom Rust code +title: Writing Native Rust Plugins for the Apollo Router +subtitle: Extend the Apollo Router with custom Rust code +description: Extend the Apollo Router with custom Rust code. Plan, build, and register plugins. Define plugin configuration and implement lifecycle hooks. --- import ElasticNotice from '../../shared/elastic-notice.mdx'; diff --git a/docs/source/customizations/overview.mdx b/docs/source/customizations/overview.mdx index bef1c895fd..a8ead13ec4 100644 --- a/docs/source/customizations/overview.mdx +++ b/docs/source/customizations/overview.mdx @@ -1,6 +1,7 @@ --- -title: Apollo Router customizations -description: Extend your router with custom functionality +title: Apollo Router Customizations +subtitle: Extend your router with custom functionality +description: Extend the Apollo Router with custom functionality. Understand the request lifecycle and how customizations intervene at specific points. --- You can create **customizations** for the Apollo Router to add functionality that isn't available via built-in [configuration options](../configuration/overview/). For example, you can make an external call to fetch authentication data for each incoming request. diff --git a/docs/source/customizations/rhai-api.mdx b/docs/source/customizations/rhai-api.mdx index b0eb1be3cd..c5ab8ebc58 100644 --- a/docs/source/customizations/rhai-api.mdx +++ b/docs/source/customizations/rhai-api.mdx @@ -1,6 +1,7 @@ --- -title: Rhai script API reference -description: For Apollo Router customizations +title: Rhai Script API Reference +subtitle: For Apollo Router customizations +description: This reference documents the symbols and behaviors that are specific to Rhai customizations for the Apollo Router. Includes entry point hooks, logging, and more. --- This reference documents the symbols and behaviors that are specific to [Rhai customizations](./rhai/) for the Apollo Router. @@ -242,24 +243,6 @@ You don't need to import the "base64" module. It is imported in the router. -## sha256 hash strings - -Your Rhai customization can use the function `sha256::digest()` to hash strings using the SHA256 hashing algorithm. - -```rhai -fn supergraph_service(service){ - service.map_request(|request|{ - let sha = sha256::digest("hello world"); - log_info(sha); - }); -} -``` - - -You don't need to import the "sha256" module. It is imported in the router. - - - ### Different alphabets Base64 supports multiple alphabets to encode data, depending on the supported characters where it is used. The router supports the following alphabets: @@ -287,6 +270,24 @@ fn supergraph_service(service) { } ``` +## sha256 hash strings + +Your Rhai customization can use the function `sha256::digest()` to hash strings using the SHA256 hashing algorithm. + +```rhai +fn supergraph_service(service){ + service.map_request(|request|{ + let sha = sha256::digest("hello world"); + log_info(sha); + }); +} +``` + + +You don't need to import the "sha256" module. It is imported in the router. + + + ## Headers with multiple values The simple get/set api for dealing with single value headers is sufficient for most use cases. If you wish to set multiple values on a key then you should do this by supplying an array of values. @@ -337,6 +338,19 @@ fn router_service(service) { +## Available constants + +The router provides constants for your Rhai scripts that mostly help you fetch data from the context. + +``` +Router.APOLLO_SDL // Context key to access the SDL +Router.APOLLO_START // Constant to calculate durations +Router.APOLLO_AUTHENTICATION_JWT_CLAIMS // Context key to access authentication jwt claims +Router.APOLLO_SUBSCRIPTION_WS_CUSTOM_CONNECTION_PARAMS // Context key to modify or access the custom connection params when using subscriptions in WebSocket to subgraphs (cf subscription docs) +Router.APOLLO_ENTITY_CACHE_KEY // Context key to access the entity cache key +Router.APOLLO_OPERATION_ID // Context key to get the value of apollo operation id (studio trace id) from the context +``` + ## `Request` interface All callback functions registered via `map_request` are passed a `request` object that represents the request sent by the client. This object provides the following fields: diff --git a/docs/source/customizations/rhai.mdx b/docs/source/customizations/rhai.mdx index a76729557c..710f4171a5 100644 --- a/docs/source/customizations/rhai.mdx +++ b/docs/source/customizations/rhai.mdx @@ -1,6 +1,7 @@ --- -title: Rhai scripts for the Apollo Router -description: Add custom functionality directly to your router +title: Rhai Scripts for the Apollo Router +subtitle: Add custom functionality directly to your router +description: Customize Apollo Router functionality with Rhai scripts. Manipulate strings, process headers and more for enhanced performance. --- You can customize the Apollo Router's behavior with scripts that use the [Rhai scripting language](https://rhai.rs/book/). In a Rust-based project, Rhai is ideal for performing common scripting tasks such as manipulating strings and processing headers. Your Rhai scripts can also hook into multiple stages of the router's [request handling lifecycle](#router-request-lifecycle). diff --git a/docs/source/enterprise-features.mdx b/docs/source/enterprise-features.mdx index 797d093822..11e45d20d6 100644 --- a/docs/source/enterprise-features.mdx +++ b/docs/source/enterprise-features.mdx @@ -1,6 +1,7 @@ --- -title: Enterprise features for the Apollo Router -description: Available with GraphOS Enterprise +title: Enterprise Features of the Apollo Router +subtitle: Connect the router to GraphOS for advanced features +description: Unlock Enterprise features for the Apollo Router by connecting it to GraphOS. Enable offline licenses for extended disconnections. --- Since the Apollo Router is [source-available](https://www.apollographql.com/blog/evaluating-apollo-router-understanding-free-and-open-vs-commercial-features), you can use its codebase without connecting it to GraphOS. GraphOS organizations with the Enterprise plan can [connect a self-hosted router to GraphOS](/graphos/quickstart/self-hosted#6-connect-the-router-to-graphos) for an expanded feature set. diff --git a/docs/source/errors.mdx b/docs/source/errors.mdx index 7b7cb8d43b..a72397aed4 100644 --- a/docs/source/errors.mdx +++ b/docs/source/errors.mdx @@ -1,7 +1,7 @@ --- title: Errors subtitle: Error and status codes returned by the Apollo Router -description: Error codes and HTTP response status codes returned by the Apollo Router +description: Reference for error and HTTP status codes returned by the Apollo Router, including explanations and solutions. --- Learn about error codes and HTTP response status codes returned by the Apollo Router. diff --git a/docs/source/executing-operations/build-run-queries.mdx b/docs/source/executing-operations/build-run-queries.mdx index fb929793a9..6303cab506 100644 --- a/docs/source/executing-operations/build-run-queries.mdx +++ b/docs/source/executing-operations/build-run-queries.mdx @@ -1,5 +1,7 @@ --- -title: Build and run queries against the Apollo Router +title: Build and Run Queries +subtitle: Use the Apollo Sandbox to build and run operations against your router +description: Use the Apollo Sandbox, a special mode of GraphOS Studio, to build and run GraphQL operations against your graph router. --- The Apollo Router serves the following landing page from its base URL (`http://localhost:4000` by default): @@ -8,6 +10,6 @@ The Apollo Router serves the following landing page from its base URL (`http://l ## Apollo Sandbox -The landing page above provides a link to [Apollo Sandbox](https://studio.apollographql.com/sandbox), a powerful web IDE that enables you to build and run operations against your router. Sandbox is a special mode of [Apollo Studio](https://www.apollographql.com/docs/studio/) that doesn't require an Apollo account. +The landing page above provides a link to [Apollo Sandbox](https://studio.apollographql.com/sandbox), a powerful web IDE that enables you to build and run operations against your router. Sandbox is a special mode of [GraphOS Studio](https://www.apollographql.com/docs/studio/) that doesn't require an Apollo account. Apollo Sandbox diff --git a/docs/source/executing-operations/defer-support.mdx b/docs/source/executing-operations/defer-support.mdx index 9d2c1b8858..65e87bc208 100644 --- a/docs/source/executing-operations/defer-support.mdx +++ b/docs/source/executing-operations/defer-support.mdx @@ -1,6 +1,7 @@ --- -title: Apollo Router support for @defer -description: Improve performance by delivering fields incrementally +title: Apollo Router Support for @defer +subtitle: Improve performance by delivering fields incrementally +description: Improve your GraphQL query performance with Apollo Router's support for the @defer directive. Incrementally deliver response data by deferring certain fields. minVersion: 1.8.0 --- diff --git a/docs/source/executing-operations/query-batching.mdx b/docs/source/executing-operations/query-batching.mdx index e9b6ed9537..7058c91249 100644 --- a/docs/source/executing-operations/query-batching.mdx +++ b/docs/source/executing-operations/query-batching.mdx @@ -1,6 +1,7 @@ --- -title: Query batching -description: Receive query batches with the Apollo Router +title: Query Batching +subtitle: Receive query batches with the Apollo Router +description: Handle multiple GraphQL requests with Apollo Router's query batching capabilities. Aggregate operations into single HTTP requests, reducing overhead. --- diff --git a/docs/source/executing-operations/requests.mdx b/docs/source/executing-operations/requests.mdx index a99f2dea1e..db6f9f88b2 100644 --- a/docs/source/executing-operations/requests.mdx +++ b/docs/source/executing-operations/requests.mdx @@ -1,6 +1,7 @@ --- -title: Operation request format -description: How to send requests to the Apollo Router over HTTP +title: Operation Request Format +subtitle: Send requests to the Apollo Router over HTTP +description: Learn how to format and send GraphQL requests to Apollo Router over HTTP. Explore POST and GET examples and the automatic persisted queries protocol. --- By default, almost every GraphQL IDE and client library takes care of sending operations in a format that the Apollo Router supports. This article describes that format, which is also described on [graphql.org](https://graphql.org/learn/serving-over-http/) and in [this preliminary spec](https://github.com/graphql/graphql-over-http). diff --git a/docs/source/executing-operations/subscription-callback-protocol.mdx b/docs/source/executing-operations/subscription-callback-protocol.mdx index dd9dd125d5..2fbf2a7431 100644 --- a/docs/source/executing-operations/subscription-callback-protocol.mdx +++ b/docs/source/executing-operations/subscription-callback-protocol.mdx @@ -1,6 +1,7 @@ --- -title: HTTP callback protocol for GraphQL subscriptions -description: For federated subgraphs communicating with the Apollo Router +title: HTTP Callback Protocol for GraphQL Subscriptions +subtitle: Enable clients to receive real-time updates via HTTP callback +description: Understand the HTTP Callback Protocol for GraphQL Subscriptions with the Apollo Router. Learn initialization, main loop, message types, and error handling. --- This reference describes a protocol for GraphQL servers (or **subgraphs**) to send subscription data to a subscribing **graph router** (such as the Apollo Router) via HTTP callbacks. Use this reference if you're adding support for this protocol to a GraphQL server library or other system. diff --git a/docs/source/executing-operations/subscription-multipart-protocol.mdx b/docs/source/executing-operations/subscription-multipart-protocol.mdx index 00e910123e..3842f3a382 100644 --- a/docs/source/executing-operations/subscription-multipart-protocol.mdx +++ b/docs/source/executing-operations/subscription-multipart-protocol.mdx @@ -1,6 +1,7 @@ --- -title: Multipart HTTP protocol for GraphQL subscriptions -description: For GraphQL clients communicating with the Apollo Router +title: Multipart HTTP protocol for GraphQL Subscriptions +subtitle: Enable clients to receive real-time updates via multipart HTTP protocol +description: Enable real-time updates via multipart HTTP protocol for GraphQL subscriptions with the Apollo Router. Learn about execution, heartbeats, and error handling. --- To execute GraphQL subscription operations on the Apollo Router, client apps do _not_ communicate over WebSocket. Instead, they use **HTTP with multipart responses**. This multipart protocol is built on the same [Incremental Delivery over HTTP](https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md) spec that the Apollo Router uses to support [the `@defer` directive](./defer-support/). diff --git a/docs/source/executing-operations/subscription-support.mdx b/docs/source/executing-operations/subscription-support.mdx index 8d22dafc44..2c0029da65 100644 --- a/docs/source/executing-operations/subscription-support.mdx +++ b/docs/source/executing-operations/subscription-support.mdx @@ -1,5 +1,5 @@ --- -title: Configure GraphQL subscription support +title: Configure GraphQL Subscription Support subtitle: Enable clients to receive real-time updates description: Configure your router to support GraphQL subscriptions, enabling clients to receive real-time updates via WebSocket or HTTP callbacks. minVersion: 1.22.0 diff --git a/docs/source/federation-version-support.mdx b/docs/source/federation-version-support.mdx index 7205400590..66194e4204 100644 --- a/docs/source/federation-version-support.mdx +++ b/docs/source/federation-version-support.mdx @@ -1,5 +1,7 @@ --- -title: Federation version support in the Apollo Router +title: Federation Version Support in the Apollo Router +subtitle: Check router version compatibility with Apollo Federation versions +description: This reference shows which version of Apollo Federation each Apollo Router release is compiled against. Ensure your router uses at least the listed federation version. --- The Apollo Router supports supergraph schemas that are generated via Apollo Federation 2.x [composition](/federation/federated-types/composition/). This composition algorithm is usually performed by one of the following: diff --git a/docs/source/index.mdx b/docs/source/index.mdx index aa9e9eb704..c4aaf69172 100644 --- a/docs/source/index.mdx +++ b/docs/source/index.mdx @@ -1,6 +1,7 @@ --- title: The Apollo Router -description: High-performance routing for self-hosted supergraphs +subtitle: High-performance routing for self-hosted supergraphs +description: Distribute operations efficiently across microservices in your federated GraphQL API with the Apollo Router. Configure caching, security features, and more. --- import { Link } from 'gatsby'; diff --git a/docs/source/managed-federation/client-awareness.mdx b/docs/source/managed-federation/client-awareness.mdx index 633b467467..29f37350a8 100644 --- a/docs/source/managed-federation/client-awareness.mdx +++ b/docs/source/managed-federation/client-awareness.mdx @@ -5,7 +5,7 @@ description: Configure client awareness in Apollo Router import { Link } from "gatsby"; -The Apollo Router supports [client awareness](/graphos/metrics/client-awareness/) by default. If the client sets the headers `apollographql-client-name` and `apollographql-client-version` in its HTTP requests, Apollo Studio can separate the metrics and queries per client. +The Apollo Router supports [client awareness](/graphos/metrics/client-awareness/) by default. If the client sets the headers `apollographql-client-name` and `apollographql-client-version` in its HTTP requests, GraphOS Studio can separate the metrics and queries per client. ## Overriding client awareness headers @@ -20,6 +20,6 @@ telemetry: client_version_header: MyClientHeaderVersion cors: # The headers to allow. - # (Defaults to [ Content-Type ], which is required for Apollo Studio) + # (Defaults to [ Content-Type ], which is required for GraphOS Studio) allow_headers: [ Content-Type, MyClientHeaderName, MyClientHeaderVersion] ``` diff --git a/docs/source/migrating-from-gateway.mdx b/docs/source/migrating-from-gateway.mdx index 995e48f1a0..6cfc30d7b3 100644 --- a/docs/source/migrating-from-gateway.mdx +++ b/docs/source/migrating-from-gateway.mdx @@ -1,6 +1,7 @@ --- -title: Gateway migration guide -description: Migrating to the Apollo Router from @apollo/gateway +title: Gateway Migration Guide +subtitle: Migrating to the Apollo Router from @apollo/gateway +description: Learn how to migrate a federated graph from @apollo/gateway to the Apollo Router without any changes to your subgraphs. --- Learn how to migrate a federated supergraph using the `@apollo/gateway` library to the Apollo Router and gain significant performance improvements while making zero changes to your subgraphs. @@ -32,7 +33,7 @@ The sections below provide more details on what to look for in each of these cat ## Environment variables -Many Apollo tools use environment variables prefixed with `APOLLO_` to set certain values, such as an API key for communicating with Apollo Studio. +Many Apollo tools use environment variables prefixed with `APOLLO_` to set certain values, such as an API key for communicating with GraphOS Studio. Make sure to note any environment variables you set in your existing gateway's environment, _especially_ those prefixed with `APOLLO_` diff --git a/docs/source/migrating-from-version-0.x.mdx b/docs/source/migrating-from-version-0.x.mdx index 246fb36709..92031d5068 100644 --- a/docs/source/migrating-from-version-0.x.mdx +++ b/docs/source/migrating-from-version-0.x.mdx @@ -1,5 +1,7 @@ --- -title: Upgrading from versions prior to 1.0 +title: Upgrading from Versions Prior to 1.0 +subtitle: Migrate from version 0.x to 1.x of the Apollo Router +description: Learn how to migrate a federated graph from pre-1.0 versions to Apollo Router 1.x for enhanced performance and streamlined configuration. --- If you were using versions of Apollo Router prior to the 1.0 release, thank you for being an early adopter! This article describes how to migrate from various pre-1.0 releases to the 1.x version. We recommend upgrading to 1.x versions and away from pre-release versions prior to 1.0 since bug-fixes and improvements will no longer be shipped to pre-1.0 versions once the 1.x version is released. @@ -67,7 +69,7 @@ The sections below provide more details on what to look for in each of these cat ## Environment variables -Many Apollo tools use environment variables prefixed with `APOLLO_` to set certain values, such as an API key for communicating with Apollo Studio. +Many Apollo tools use environment variables prefixed with `APOLLO_` to set certain values, such as an API key for communicating with GraphOS Studio. Make sure to note any environment variables you set in your existing gateway's environment, _especially_ those prefixed with `APOLLO_` diff --git a/docs/source/privacy.mdx b/docs/source/privacy.mdx index 05326fe2ec..823dfe4f75 100644 --- a/docs/source/privacy.mdx +++ b/docs/source/privacy.mdx @@ -1,5 +1,7 @@ --- -title: Privacy and data collection +title: Privacy and Data Collection +subtitle: Learn what data the Apollo Router collects and how to opt out +description: Learn what data the Apollo Router collects and how to opt out. By default, the Apollo Router collects anonymous usage data to help improve the product. --- By default, the Apollo Router collects anonymous usage data to help us improve the product. diff --git a/docs/source/quickstart.mdx b/docs/source/quickstart.mdx index 42914968ea..9ab5303a9e 100644 --- a/docs/source/quickstart.mdx +++ b/docs/source/quickstart.mdx @@ -1,6 +1,7 @@ --- -title: Apollo Router quickstart -description: Run the Apollo Router with Apollo-hosted subgraphs +title: Apollo Router Quickstart +subtitle: Run the Apollo Router with Apollo-hosted subgraphs +description: This quickstart tutorial walks you through installing the Apollo Router and running it in front of some Apollo-hosted example subgraphs. --- import ElasticNotice from '../shared/elastic-notice.mdx'; diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 8151da867a..930f0825b2 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -20,7 +20,7 @@ reqwest = { workspace = true, features = ["json", "blocking"] } serde_json.workspace = true tokio.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.5.21+v2.7.5" +router-bridge = "=0.5.25+v2.8.0" [dev-dependencies] anyhow = "1" diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml index 06b900da23..1774ed688d 100644 --- a/helm/chart/router/Chart.yaml +++ b/helm/chart/router/Chart.yaml @@ -20,10 +20,10 @@ type: application # so it matches the shape of our release process and release automation. # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix # of "v" is not included. -version: 1.47.0 +version: 1.48.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.47.0" +appVersion: "v1.48.0" diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md index eed82179b9..8f415c7ac9 100644 --- a/helm/chart/router/README.md +++ b/helm/chart/router/README.md @@ -2,7 +2,7 @@ [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation -![Version: 1.47.0](https://img.shields.io/badge/Version-1.47.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.47.0](https://img.shields.io/badge/AppVersion-v1.47.0-informational?style=flat-square) +![Version: 1.48.0](https://img.shields.io/badge/Version-1.48.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.48.0](https://img.shields.io/badge/AppVersion-v1.48.0-informational?style=flat-square) ## Prerequisites @@ -11,7 +11,7 @@ ## Get Repo Info ```console -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.47.0 +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.48.0 ``` ## Install Chart @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.47.0 **Important:** only helm3 is supported ```console -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.47.0 --values my-values.yaml +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.48.0 --values my-values.yaml ``` _See [configuration](#configuration) below._ @@ -95,4 +95,4 @@ helm show values oci://ghcr.io/apollographql/helm-charts/router | virtualservice.enabled | bool | `false` | | ---------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.13.1](https://github.com/norwoodj/helm-docs/releases/v1.13.1) +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/licenses.html b/licenses.html index 9461358688..58c0e2eae0 100644 --- a/licenses.html +++ b/licenses.html @@ -48,8 +48,8 @@

Overview of licenses:

  • MIT License (152)
  • BSD 3-Clause "New" or "Revised" License (12)
  • ISC License (11)
  • -
  • Elastic License 2.0 (6)
  • BSD 2-Clause "Simplified" License (3)
  • +
  • Elastic License 2.0 (3)
  • Mozilla Public License 2.0 (3)
  • Creative Commons Zero v1.0 Universal (2)
  • OpenSSL License (2)
  • @@ -4063,6 +4063,7 @@

    Used by:

  • toml
  • toml_datetime
  • toml_edit
  • +
  • toml_edit
  • trust-dns-proto
  • trust-dns-resolver
  • unreachable
  • @@ -8363,7 +8364,6 @@

    Used by:

  • threadpool
  • tikv-jemalloc-sys
  • tikv-jemallocator
  • -
  • toml_edit
  • triomphe
  • try_match
  • tungstenite
  • @@ -12685,7 +12685,7 @@

    Used by:

    BSD 3-Clause "New" or "Revised" License

    Used by:

    Copyright (c) 2016-2021 isis agora lovecruft. All rights reserved.
     Copyright (c) 2016-2021 Henry de Valence. All rights reserved.
    @@ -13163,9 +13163,6 @@ 

    Elastic License 2.0

    Used by:

    Copyright 2021 Apollo Graph, Inc.
     
    diff --git a/scripts/install.sh b/scripts/install.sh
    index 3b509ebc6b..c031121259 100755
    --- a/scripts/install.sh
    +++ b/scripts/install.sh
    @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa
     
     # Router version defined in apollo-router's Cargo.toml
     # Note: Change this line manually during the release steps.
    -PACKAGE_VERSION="v1.47.0"
    +PACKAGE_VERSION="v1.48.0"
     
     download_binary() {
         downloader --check