From a1b638deca9b9648228af862a528200203b880e3 Mon Sep 17 00:00:00 2001 From: bryn Date: Thu, 3 Oct 2024 13:36:14 +0100 Subject: [PATCH 01/35] Add `experimental_datadog_agent_sampling` This mode will change the behaviour of the router for tracing in the following ways: * Spans are never dropped, instead they are converted to RecordOnly. * Spans that are sent to otlp and datadog exporters will always look like they have been sampled. * The `sampling.priority` attribute is populated on spans. * `psr` is populated in trace state. * `m` is populated in trace state. --- ...datadog_upstream_sampling_decision_test.md | 48 + ...nfiguration__tests__schema_generation.snap | 6 + apollo-router/src/plugins/telemetry/config.rs | 30 +- apollo-router/src/plugins/telemetry/mod.rs | 110 ++- .../src/plugins/telemetry/otel/layer.rs | 4 +- .../src/plugins/telemetry/otel/tracer.rs | 9 +- ....field_instrumentation_sampler.router.yaml | 11 + .../tracing/datadog/agent_sampling.rs | 376 ++++++++ .../tracing/{datadog.rs => datadog/mod.rs} | 36 +- .../tracing/datadog/span_processor.rs | 133 +++ .../datadog_exporter/exporter/model/v05.rs | 21 +- .../telemetry/tracing/datadog_exporter/mod.rs | 189 ++-- .../src/plugins/telemetry/tracing/mod.rs | 8 + .../src/plugins/telemetry/tracing/otlp.rs | 27 +- apollo-router/tests/common.rs | 59 +- apollo-router/tests/integration/mod.rs | 9 + .../tests/integration/telemetry/datadog.rs | 418 ++++++++- .../telemetry/fixtures/datadog.router.yaml | 1 + ...atadog_agent_sampling_disabled.router.yaml | 23 + .../datadog_default_span_names.router.yaml | 1 + .../datadog_no_parent_sampler.router.yaml | 28 + .../fixtures/datadog_no_sample.router.yaml | 1 + .../datadog_override_span_names.router.yaml | 1 + ...tadog_override_span_names_late.router.yaml | 1 + ...tadog_resource_mapping_default.router.yaml | 1 + ...adog_resource_mapping_override.router.yaml | 1 + .../telemetry/fixtures/otlp.router.yaml | 14 +- .../otlp_datadog_agent_no_sample.router.yaml | 42 + .../otlp_datadog_agent_sample.router.yaml | 42 + ...datadog_agent_sample_no_sample.router.yaml | 42 + .../otlp_datadog_propagation.router.yaml | 39 + ...p_datadog_propagation_no_agent.router.yaml | 38 + ..._propagation_no_parent_sampler.router.yaml | 40 + ...request_with_zipkin_propagator.router.yaml | 40 + .../otlp_no_parent_sampler.router.yaml | 25 + .../tests/integration/telemetry/jaeger.rs | 8 +- .../tests/integration/telemetry/otlp.rs | 876 +++++++++++++++--- .../telemetry/exporters/tracing/datadog.mdx | 65 +- 38 files changed, 2490 insertions(+), 333 deletions(-) create mode 100644 .changesets/fix_bryn_datadog_upstream_sampling_decision_test.md create mode 100644 apollo-router/src/plugins/telemetry/testdata/config.field_instrumentation_sampler.router.yaml create mode 100644 apollo-router/src/plugins/telemetry/tracing/datadog/agent_sampling.rs rename apollo-router/src/plugins/telemetry/tracing/{datadog.rs => datadog/mod.rs} (93%) create mode 100644 apollo-router/src/plugins/telemetry/tracing/datadog/span_processor.rs create mode 100644 apollo-router/tests/integration/telemetry/fixtures/datadog_agent_sampling_disabled.router.yaml create mode 100644 apollo-router/tests/integration/telemetry/fixtures/datadog_no_parent_sampler.router.yaml create mode 100644 apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_agent_no_sample.router.yaml create mode 100644 apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_agent_sample.router.yaml create mode 100644 apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_agent_sample_no_sample.router.yaml create mode 100644 apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_propagation.router.yaml create mode 100644 apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_propagation_no_agent.router.yaml create mode 100644 apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_propagation_no_parent_sampler.router.yaml create mode 100644 apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_request_with_zipkin_propagator.router.yaml create mode 100644 apollo-router/tests/integration/telemetry/fixtures/otlp_no_parent_sampler.router.yaml diff --git a/.changesets/fix_bryn_datadog_upstream_sampling_decision_test.md b/.changesets/fix_bryn_datadog_upstream_sampling_decision_test.md new file mode 100644 index 0000000000..bb6447a66b --- /dev/null +++ b/.changesets/fix_bryn_datadog_upstream_sampling_decision_test.md @@ -0,0 +1,48 @@ +### Respect x-datadog-sampling-priority ([PR #6017](https://github.com/apollographql/router/pull/6017)) + +This PR consists of two fixes: +#### Datadog priority sampling resolution is not lost. + +Previously a `x-datadog-sampling-priority` of `-1` would be converted to `0` for downstream requests and `2` would be converted to `1`. + +#### The sampler option in the `telemetry.exporters.tracing.common.sampler` is not datadog aware. + +To get accurate APM metrics all spans must be sent to the datadog agent with a `psr` or `sampling.priority` attribute set appropriately to record the sampling decision. + +`preview_datadog_agent_sampling` option in the router.yaml enables this behavior and should be used when exporting to the datadog agent via OTLP or datadog native. + +```yaml +telemetry: + exporters: + tracing: + common: + # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + sampler: 0.1 + # Send all spans to the Datadog agent. + preview_datadog_agent_sampling: true + + # Example OTLP exporter configuration + otlp: + enabled: true + # Optional batch processor setting, this will enable the batch processor to send concurrent requests in a high load scenario. + batch_processor: + max_concurrent_exports: 100 + + # Example Datadog native exporter configuration + datadog: + enabled: true + + # Optional batch processor setting, this will enable the batch processor to send concurrent requests in a high load scenario. + batch_processor: + max_concurrent_exports: 100 +``` + +By using these options, you can decrease your Datadog bill as you will only be sending a percentage of spans from the Datadog agent to datadog. + +> [!IMPORTANT] +> Users must enable `preview_datadog_agent_sampling` to get accurate APM metrics. + +> [!IMPORTANT] +> Sending all spans to the datadog agent may require that you tweak the `batch_processor` settings in your exporter config. This applies to both OTLP and the Datadog native exporter. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/6017 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 4eba0206d0..af25ac3158 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 @@ -7306,6 +7306,12 @@ expression: "&schema" "description": "Whether to use parent based sampling", "type": "boolean" }, + "preview_datadog_agent_sampling": { + "default": null, + "description": "Use datadog agent sampling. This means that all spans will be sent to the Datadog agent and the `sampling.priority` attribute will be used to control if the span will then be sent to Datadog", + "nullable": true, + "type": "boolean" + }, "resource": { "additionalProperties": { "$ref": "#/definitions/AttributeValue", diff --git a/apollo-router/src/plugins/telemetry/config.rs b/apollo-router/src/plugins/telemetry/config.rs index 4c9be01135..8dc84e85c0 100644 --- a/apollo-router/src/plugins/telemetry/config.rs +++ b/apollo-router/src/plugins/telemetry/config.rs @@ -24,6 +24,7 @@ use super::*; use crate::plugin::serde::deserialize_option_header_name; use crate::plugins::telemetry::metrics; use crate::plugins::telemetry::resource::ConfigResource; +use crate::plugins::telemetry::tracing::datadog::DatadogAgentSampling; use crate::Configuration; #[derive(thiserror::Error, Debug)] @@ -347,6 +348,9 @@ pub(crate) struct TracingCommon { pub(crate) service_namespace: Option, /// The sampler, always_on, always_off or a decimal between 0.0 and 1.0 pub(crate) sampler: SamplerOption, + /// Use datadog agent sampling. This means that all spans will be sent to the Datadog agent + /// and the `sampling.priority` attribute will be used to control if the span will then be sent to Datadog + pub(crate) preview_datadog_agent_sampling: Option, /// Whether to use parent based sampling pub(crate) parent_based_sampler: bool, /// The maximum events per span before discarding @@ -401,6 +405,7 @@ impl Default for TracingCommon { service_name: Default::default(), service_namespace: Default::default(), sampler: default_sampler(), + preview_datadog_agent_sampling: None, parent_based_sampler: default_parent_based_sampler(), max_events_per_span: default_max_events_per_span(), max_attributes_per_span: default_max_attributes_per_span(), @@ -668,8 +673,15 @@ impl From<&TracingCommon> for opentelemetry::sdk::trace::Config { if config.parent_based_sampler { sampler = parent_based(sampler); } + if config.preview_datadog_agent_sampling.unwrap_or_default() { + common = common.with_sampler(DatadogAgentSampling::new( + sampler, + config.parent_based_sampler, + )); + } else { + common = common.with_sampler(sampler); + } - common = common.with_sampler(sampler); common = common.with_max_events_per_span(config.max_events_per_span); common = common.with_max_attributes_per_span(config.max_attributes_per_span); common = common.with_max_links_per_span(config.max_links_per_span); @@ -688,6 +700,22 @@ fn parent_based(sampler: opentelemetry::sdk::trace::Sampler) -> opentelemetry::s impl Conf { pub(crate) fn calculate_field_level_instrumentation_ratio(&self) -> Result { + // Because when datadog is enabled the global sampling is overriden to always_on + if self + .exporters + .tracing + .common + .preview_datadog_agent_sampling + .unwrap_or_default() + { + let field_ratio = match &self.apollo.field_level_instrumentation_sampler { + SamplerOption::TraceIdRatioBased(ratio) => *ratio, + SamplerOption::Always(Sampler::AlwaysOn) => 1.0, + SamplerOption::Always(Sampler::AlwaysOff) => 0.0, + }; + + return Ok(field_ratio); + } Ok( match ( &self.exporters.tracing.common.sampler, diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index 1478261be5..fa6fa6494f 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -286,6 +286,20 @@ impl Plugin for Telemetry { .expect("otel error handler lock poisoned, fatal"); let mut config = init.config; + // This code would have enabled datadog agent sampling by default, but for now we will leave it as opt-in. + // If the datadog exporter is enabled then enable the agent sampler. + // If users are using otlp export then they will need to set this explicitly in their config. + // + // if config.exporters.tracing.datadog.enabled() + // && config + // .exporters + // .tracing + // .common + // .preview_datadog_agent_sampling + // .is_none() + // { + // config.exporters.tracing.common.preview_datadog_agent_sampling = Some(true); + // } config.instrumentation.spans.update_defaults(); config.instrumentation.instruments.update_defaults(); config.exporters.logging.validate()?; @@ -866,7 +880,21 @@ impl Telemetry { // Only apply things if we were executing in the context of a vanilla the Apollo executable. // Users that are rolling their own routers will need to set up telemetry themselves. if let Some(hot_tracer) = OPENTELEMETRY_TRACER_HANDLE.get() { - otel::layer::configure(&self.sampling_filter_ratio); + // If the datadog agent sampling is enabled, then we cannot presample the spans + // Therefore we set presampling to always on and let the regular sampler do the work. + // Effectively, we are disabling the presampling. + if self + .config + .exporters + .tracing + .common + .preview_datadog_agent_sampling + .unwrap_or_default() + { + otel::layer::configure(&SamplerOption::Always(Sampler::AlwaysOn)); + } else { + otel::layer::configure(&self.sampling_filter_ratio); + } // The reason that this has to happen here is that we are interacting with global state. // If we do this logic during plugin init then if a subsequent plugin fails to init then we @@ -889,7 +917,8 @@ impl Telemetry { Self::checked_global_tracer_shutdown(last_provider); - opentelemetry::global::set_text_map_propagator(Self::create_propagator(&self.config)); + let propagator = Self::create_propagator(&self.config); + opentelemetry::global::set_text_map_propagator(propagator); } activation.reload_metrics(); @@ -934,9 +963,6 @@ impl Telemetry { if propagation.zipkin || tracing.zipkin.enabled { propagators.push(Box::::default()); } - if propagation.datadog || tracing.datadog.enabled() { - propagators.push(Box::::default()); - } if propagation.aws_xray { propagators.push(Box::::default()); } @@ -946,6 +972,9 @@ impl Telemetry { propagation.request.format.clone(), ))); } + if propagation.datadog || tracing.datadog.enabled() { + propagators.push(Box::::default()); + } TextMapCompositePropagator::new(propagators) } @@ -957,9 +986,14 @@ impl Telemetry { let spans_config = &config.instrumentation.spans; let mut common = tracing_config.common.clone(); let mut sampler = common.sampler.clone(); - // set it to AlwaysOn: it is now done in the SamplingFilter, so whatever is sent to an exporter - // should be accepted - common.sampler = SamplerOption::Always(Sampler::AlwaysOn); + + // To enable pre-sampling to work we need to disable regular sampling. + // This is because the pre-sampler will sample the spans before they sent to the regular sampler + // If the datadog agent sampling is enabled, then we cannot pre-sample the spans because even if the sampling decision is made to drop + // DatadogAgentSampler will modify the decision to RecordAndSample and instead use the sampling.priority attribute to decide if the span should be sampled or not. + if !common.preview_datadog_agent_sampling.unwrap_or_default() { + common.sampler = SamplerOption::Always(Sampler::AlwaysOn); + } let mut builder = opentelemetry::sdk::trace::TracerProvider::builder().with_config((&common).into()); @@ -2130,6 +2164,8 @@ mod tests { use std::collections::HashMap; use std::fmt::Debug; use std::ops::DerefMut; + use std::sync::atomic::AtomicUsize; + use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; @@ -2187,6 +2223,7 @@ mod tests { use crate::plugins::demand_control::COST_STRATEGY_KEY; use crate::plugins::telemetry::config::TraceIdFormat; use crate::plugins::telemetry::handle_error_internal; + use crate::plugins::telemetry::EnableSubgraphFtv1; use crate::services::router::body::get_body_bytes; use crate::services::RouterRequest; use crate::services::RouterResponse; @@ -2832,6 +2869,63 @@ mod tests { .await; } + #[tokio::test] + async fn test_field_instrumentation_sampler_with_preview_datadog_agent_sampling() { + let plugin = create_plugin_with_config(include_str!( + "testdata/config.field_instrumentation_sampler.router.yaml" + )) + .await; + + let ftv1_counter = Arc::new(AtomicUsize::new(0)); + let ftv1_counter_cloned = ftv1_counter.clone(); + + let mut mock_request_service = MockSupergraphService::new(); + mock_request_service + .expect_call() + .times(10) + .returning(move |req: SupergraphRequest| { + if req + .context + .extensions() + .with_lock(|lock| lock.contains_key::()) + { + ftv1_counter_cloned.fetch_add(1, Ordering::Relaxed); + } + Ok(SupergraphResponse::fake_builder() + .context(req.context) + .status_code(StatusCode::OK) + .header("content-type", "application/json") + .data(json!({"errors": [{"message": "nope"}]})) + .build() + .unwrap()) + }); + let mut request_supergraph_service = + plugin.supergraph_service(BoxService::new(mock_request_service)); + + for _ in 0..10 { + let supergraph_req = SupergraphRequest::fake_builder() + .header("x-custom", "TEST") + .header("conditional-custom", "X") + .header("custom-length", "55") + .header("content-length", "55") + .header("content-type", "application/graphql") + .query("Query test { me {name} }") + .operation_name("test".to_string()); + let _router_response = request_supergraph_service + .ready() + .await + .unwrap() + .call(supergraph_req.build().unwrap()) + .await + .unwrap() + .next_response() + .await + .unwrap(); + } + // It should be 100% because when we set preview_datadog_agent_sampling, we only take the value of field_level_instrumentation_sampler + assert_eq!(ftv1_counter.load(Ordering::Relaxed), 10); + } + #[tokio::test] async fn test_subgraph_metrics_ok() { async { diff --git a/apollo-router/src/plugins/telemetry/otel/layer.rs b/apollo-router/src/plugins/telemetry/otel/layer.rs index 866bf50a35..86415d2b4d 100644 --- a/apollo-router/src/plugins/telemetry/otel/layer.rs +++ b/apollo-router/src/plugins/telemetry/otel/layer.rs @@ -677,13 +677,13 @@ pub(crate) fn configure(sampler: &SamplerOption) { }, }; - SPAN_SAMPLING_RATE.store(f64::to_bits(ratio), Ordering::Relaxed); + SPAN_SAMPLING_RATE.store(f64::to_bits(ratio), Ordering::SeqCst); } impl OpenTelemetryLayer { fn sample(&self) -> bool { let s: f64 = thread_rng().gen_range(0.0..=1.0); - s <= f64::from_bits(SPAN_SAMPLING_RATE.load(Ordering::Relaxed)) + s <= f64::from_bits(SPAN_SAMPLING_RATE.load(Ordering::SeqCst)) } } diff --git a/apollo-router/src/plugins/telemetry/otel/tracer.rs b/apollo-router/src/plugins/telemetry/otel/tracer.rs index 463fd8cb2c..6b11bab9ad 100644 --- a/apollo-router/src/plugins/telemetry/otel/tracer.rs +++ b/apollo-router/src/plugins/telemetry/otel/tracer.rs @@ -16,7 +16,6 @@ use opentelemetry_sdk::trace::Tracer as SdkTracer; use opentelemetry_sdk::trace::TracerProvider as SdkTracerProvider; use super::OtelData; -use crate::plugins::telemetry::tracing::datadog_exporter::DatadogTraceState; /// An interface for authors of OpenTelemetry SDKs to build pre-sampled tracers. /// @@ -81,6 +80,7 @@ impl PreSampledTracer for SdkTracer { let parent_cx = &data.parent_cx; let builder = &mut data.builder; + // If we have a parent span that means we have a parent span coming from a propagator // Gather trace state let (trace_id, parent_trace_flags) = current_trace_state(builder, parent_cx, &provider); @@ -159,12 +159,7 @@ fn process_sampling_result( decision: SamplingDecision::RecordAndSample, trace_state, .. - } => Some(( - trace_flags | TraceFlags::SAMPLED, - trace_state - .with_priority_sampling(true) - .with_measuring(true), - )), + } => Some((trace_flags | TraceFlags::SAMPLED, trace_state.clone())), } } diff --git a/apollo-router/src/plugins/telemetry/testdata/config.field_instrumentation_sampler.router.yaml b/apollo-router/src/plugins/telemetry/testdata/config.field_instrumentation_sampler.router.yaml new file mode 100644 index 0000000000..54f4167b22 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/testdata/config.field_instrumentation_sampler.router.yaml @@ -0,0 +1,11 @@ +telemetry: + instrumentation: + spans: + mode: spec_compliant + apollo: + field_level_instrumentation_sampler: 1.0 + exporters: + tracing: + common: + preview_datadog_agent_sampling: true + sampler: 0.5 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/tracing/datadog/agent_sampling.rs b/apollo-router/src/plugins/telemetry/tracing/datadog/agent_sampling.rs new file mode 100644 index 0000000000..2fc04e94bd --- /dev/null +++ b/apollo-router/src/plugins/telemetry/tracing/datadog/agent_sampling.rs @@ -0,0 +1,376 @@ +use opentelemetry_api::trace::Link; +use opentelemetry_api::trace::SamplingDecision; +use opentelemetry_api::trace::SamplingResult; +use opentelemetry_api::trace::SpanKind; +use opentelemetry_api::trace::TraceId; +use opentelemetry_api::Key; +use opentelemetry_api::KeyValue; +use opentelemetry_api::OrderMap; +use opentelemetry_api::Value; +use opentelemetry_sdk::trace::ShouldSample; + +use crate::plugins::telemetry::tracing::datadog_exporter::propagator::SamplingPriority; +use crate::plugins::telemetry::tracing::datadog_exporter::DatadogTraceState; + +/// The Datadog Agent Sampler +/// +/// This sampler overrides the sampling decision to ensure that spans are recorded even if they were originally dropped. +/// It performs the following tasks: +/// 1. Ensures the appropriate trace state is set +/// 2. Adds the sampling.priority attribute to the span +/// +/// The sampler can be configured to use parent-based sampling for consistent trace sampling. +/// +#[derive(Debug, Clone)] +pub(crate) struct DatadogAgentSampling { + /// The underlying sampler used for initial sampling decisions + pub(crate) sampler: opentelemetry::sdk::trace::Sampler, + /// Flag to enable parent-based sampling for consistent trace sampling + pub(crate) parent_based_sampler: bool, +} +impl DatadogAgentSampling { + /// Creates a new DatadogAgentSampling instance + /// + /// # Arguments + /// * `sampler` - The underlying sampler to use for initial sampling decisions + /// * `parent_based_sampler` - Whether to use parent-based sampling for consistent trace sampling + pub(crate) fn new( + sampler: opentelemetry::sdk::trace::Sampler, + parent_based_sampler: bool, + ) -> Self { + Self { + sampler, + parent_based_sampler, + } + } +} + +impl ShouldSample for DatadogAgentSampling { + fn should_sample( + &self, + parent_context: Option<&opentelemetry_api::Context>, + trace_id: TraceId, + name: &str, + span_kind: &SpanKind, + attributes: &OrderMap, + links: &[Link], + ) -> SamplingResult { + let mut result = self.sampler.should_sample( + parent_context, + trace_id, + name, + span_kind, + attributes, + links, + ); + // Override the sampling decision to record and make sure that the trace state is set correctly + // if either parent sampling is disabled or it has not been populated by a propagator. + // The propagator gets first dibs on setting the trace state, so if it sets it, we don't override it unless we are not parent based. + match result.decision { + SamplingDecision::Drop | SamplingDecision::RecordOnly => { + result.decision = SamplingDecision::RecordOnly; + if !self.parent_based_sampler || result.trace_state.sampling_priority().is_none() { + result.trace_state = result + .trace_state + .with_priority_sampling(SamplingPriority::AutoReject) + } + } + SamplingDecision::RecordAndSample => { + if !self.parent_based_sampler || result.trace_state.sampling_priority().is_none() { + result.trace_state = result + .trace_state + .with_priority_sampling(SamplingPriority::AutoKeep) + } + } + } + + // We always want to measure + result.trace_state = result.trace_state.with_measuring(true); + // We always want to set the sampling.priority attribute in case we are communicating with the agent via otlp. + // Reverse engineered from https://github.com/DataDog/datadog-agent/blob/c692f62423f93988b008b669008f9199a5ad196b/pkg/trace/api/otlp.go#L502 + result.attributes.push(KeyValue::new( + "sampling.priority", + Value::I64( + result + .trace_state + .sampling_priority() + .expect("sampling priority") + .as_i64(), + ), + )); + result + } +} +#[cfg(test)] +mod tests { + use buildstructor::Builder; + use opentelemetry::sdk::trace::Sampler; + use opentelemetry::trace::TraceState; + use opentelemetry_api::trace::Link; + use opentelemetry_api::trace::SamplingDecision; + use opentelemetry_api::trace::SamplingResult; + use opentelemetry_api::trace::SpanContext; + use opentelemetry_api::trace::SpanId; + use opentelemetry_api::trace::SpanKind; + use opentelemetry_api::trace::TraceContextExt; + use opentelemetry_api::trace::TraceFlags; + use opentelemetry_api::trace::TraceId; + use opentelemetry_api::Context; + use opentelemetry_api::Key; + use opentelemetry_api::OrderMap; + use opentelemetry_api::Value; + use opentelemetry_sdk::trace::ShouldSample; + + use crate::plugins::telemetry::tracing::datadog::DatadogAgentSampling; + use crate::plugins::telemetry::tracing::datadog_exporter::propagator::SamplingPriority; + use crate::plugins::telemetry::tracing::datadog_exporter::DatadogTraceState; + + #[derive(Debug, Clone, Builder)] + struct StubSampler { + decision: SamplingDecision, + } + + impl ShouldSample for StubSampler { + fn should_sample( + &self, + _parent_context: Option<&Context>, + _trace_id: TraceId, + _name: &str, + _span_kind: &SpanKind, + _attributes: &OrderMap, + _links: &[Link], + ) -> SamplingResult { + SamplingResult { + decision: self.decision.clone(), + attributes: Vec::new(), + trace_state: Default::default(), + } + } + } + + #[test] + fn test_should_sample_drop() { + // Test case where the sampling decision is Drop + let sampler = StubSampler::builder() + .decision(SamplingDecision::Drop) + .build(); + let datadog_sampler = + DatadogAgentSampling::new(Sampler::ParentBased(Box::new(sampler)), false); + + let result = datadog_sampler.should_sample( + None, + TraceId::from_u128(1), + "test_span", + &SpanKind::Internal, + &OrderMap::new(), + &[], + ); + + // Verify that the decision is RecordOnly (converted from Drop) + assert_eq!(result.decision, SamplingDecision::RecordOnly); + // Verify that the sampling priority is set to AutoReject + assert_eq!( + result.trace_state.sampling_priority(), + Some(SamplingPriority::AutoReject) + ); + // Verify that the sampling.priority attribute is set correctly + assert!(result + .attributes + .iter() + .any(|kv| kv.key.as_str() == "sampling.priority" + && kv.value == Value::I64(SamplingPriority::AutoReject.as_i64()))); + + // Verify that measuring is enabled + assert!(result.trace_state.measuring_enabled()); + } + + #[test] + fn test_should_sample_record_only() { + let sampler = StubSampler::builder() + .decision(SamplingDecision::RecordOnly) + .build(); + let datadog_sampler = + DatadogAgentSampling::new(Sampler::ParentBased(Box::new(sampler)), false); + + let result = datadog_sampler.should_sample( + None, + TraceId::from_u128(1), + "test_span", + &SpanKind::Internal, + &OrderMap::new(), + &[], + ); + + // Record only should remain as record only + assert_eq!(result.decision, SamplingDecision::RecordOnly); + + // Verify that the sampling priority is set to AutoReject so the trace won't be transmitted to Datadog + assert_eq!( + result.trace_state.sampling_priority(), + Some(SamplingPriority::AutoReject) + ); + assert!(result + .attributes + .iter() + .any(|kv| kv.key.as_str() == "sampling.priority" + && kv.value == Value::I64(SamplingPriority::AutoReject.as_i64()))); + + // Verify that measuring is enabled + assert!(result.trace_state.measuring_enabled()); + } + + #[test] + fn test_should_sample_record_and_sample() { + let sampler = StubSampler::builder() + .decision(SamplingDecision::RecordAndSample) + .build(); + let datadog_sampler = + DatadogAgentSampling::new(Sampler::ParentBased(Box::new(sampler)), false); + + let result = datadog_sampler.should_sample( + None, + TraceId::from_u128(1), + "test_span", + &SpanKind::Internal, + &OrderMap::new(), + &[], + ); + + // Record and sample should remain as record and sample + assert_eq!(result.decision, SamplingDecision::RecordAndSample); + + // Verify that the sampling priority is set to AutoKeep so the trace will be transmitted to Datadog + assert_eq!( + result.trace_state.sampling_priority(), + Some(SamplingPriority::AutoKeep) + ); + assert!(result + .attributes + .iter() + .any(|kv| kv.key.as_str() == "sampling.priority" + && kv.value == Value::I64(SamplingPriority::AutoKeep.as_i64()))); + + // Verify that measuring is enabled + assert!(result.trace_state.measuring_enabled()); + } + + #[test] + fn test_should_sample_with_parent_based_sampler() { + let sampler = StubSampler::builder() + .decision(SamplingDecision::RecordAndSample) + .build(); + + let datadog_sampler = + DatadogAgentSampling::new(Sampler::ParentBased(Box::new(sampler)), true); + + let result = datadog_sampler.should_sample( + Some(&Context::new()), + TraceId::from_u128(1), + "test_span", + &SpanKind::Internal, + &OrderMap::new(), + &[], + ); + + // Record and sample should remain as record and sample + assert_eq!(result.decision, SamplingDecision::RecordAndSample); + + // Verify that the sampling priority is set to AutoKeep so the trace will be transmitted to Datadog + assert_eq!( + result.trace_state.sampling_priority(), + Some(SamplingPriority::AutoKeep) + ); + assert!(result + .attributes + .iter() + .any(|kv| kv.key.as_str() == "sampling.priority" + && kv.value == Value::I64(SamplingPriority::AutoKeep.as_i64()))); + + // Verify that measuring is enabled + assert!(result.trace_state.measuring_enabled()); + } + + #[test] + fn test_trace_state_already_populated_record_and_sample() { + let sampler = StubSampler::builder() + .decision(SamplingDecision::RecordAndSample) + .build(); + + let datadog_sampler = + DatadogAgentSampling::new(Sampler::ParentBased(Box::new(sampler)), true); + + let result = datadog_sampler.should_sample( + Some(&Context::new().with_remote_span_context(SpanContext::new( + TraceId::from_u128(1), + SpanId::from_u64(1), + TraceFlags::SAMPLED, + true, + TraceState::default().with_priority_sampling(SamplingPriority::UserReject), + ))), + TraceId::from_u128(1), + "test_span", + &SpanKind::Internal, + &OrderMap::new(), + &[], + ); + + // Record and sample should remain as record and sample + assert_eq!(result.decision, SamplingDecision::RecordAndSample); + + // Verify that the sampling priority is not overridden + assert_eq!( + result.trace_state.sampling_priority(), + Some(SamplingPriority::UserReject) + ); + assert!(result + .attributes + .iter() + .any(|kv| kv.key.as_str() == "sampling.priority" + && kv.value == Value::I64(SamplingPriority::UserReject.as_i64()))); + + // Verify that measuring is enabled + assert!(result.trace_state.measuring_enabled()); + } + + #[test] + fn test_trace_state_already_populated_record_drop() { + let sampler = StubSampler::builder() + .decision(SamplingDecision::Drop) + .build(); + + let datadog_sampler = + DatadogAgentSampling::new(Sampler::ParentBased(Box::new(sampler)), true); + + let result = datadog_sampler.should_sample( + Some(&Context::new().with_remote_span_context(SpanContext::new( + TraceId::from_u128(1), + SpanId::from_u64(1), + TraceFlags::default(), + true, + TraceState::default().with_priority_sampling(SamplingPriority::UserReject), + ))), + TraceId::from_u128(1), + "test_span", + &SpanKind::Internal, + &OrderMap::new(), + &[], + ); + + // Drop is converted to RecordOnly + assert_eq!(result.decision, SamplingDecision::RecordOnly); + + // Verify that the sampling priority is not overridden + assert_eq!( + result.trace_state.sampling_priority(), + Some(SamplingPriority::UserReject) + ); + assert!(result + .attributes + .iter() + .any(|kv| kv.key.as_str() == "sampling.priority" + && kv.value == Value::I64(SamplingPriority::UserReject.as_i64()))); + + // Verify that measuring is enabled + assert!(result.trace_state.measuring_enabled()); + } +} diff --git a/apollo-router/src/plugins/telemetry/tracing/datadog.rs b/apollo-router/src/plugins/telemetry/tracing/datadog/mod.rs similarity index 93% rename from apollo-router/src/plugins/telemetry/tracing/datadog.rs rename to apollo-router/src/plugins/telemetry/tracing/datadog/mod.rs index 4574b529ff..66fc09f108 100644 --- a/apollo-router/src/plugins/telemetry/tracing/datadog.rs +++ b/apollo-router/src/plugins/telemetry/tracing/datadog/mod.rs @@ -1,15 +1,18 @@ //! Configuration for datadog tracing. +mod agent_sampling; +mod span_processor; + use std::fmt::Debug; use std::fmt::Formatter; use std::time::Duration; +pub(crate) use agent_sampling::DatadogAgentSampling; use ahash::HashMap; use ahash::HashMapExt; use futures::future::BoxFuture; use http::Uri; use opentelemetry::sdk; -use opentelemetry::sdk::trace::BatchSpanProcessor; use opentelemetry::sdk::trace::Builder; use opentelemetry::Value; use opentelemetry_api::trace::SpanContext; @@ -23,6 +26,7 @@ use opentelemetry_semantic_conventions::resource::SERVICE_NAME; use opentelemetry_semantic_conventions::resource::SERVICE_VERSION; use schemars::JsonSchema; use serde::Deserialize; +pub(crate) use span_processor::DatadogSpanProcessor; use tower::BoxError; use crate::plugins::telemetry::config::GenericWith; @@ -210,18 +214,24 @@ impl TracingConfigurator for Config { let mut span_metrics = default_span_metrics(); span_metrics.extend(self.span_metrics.clone()); - Ok(builder.with_span_processor( - BatchSpanProcessor::builder( - ExporterWrapper { - delegate: exporter, - span_metrics, - }, - opentelemetry::runtime::Tokio, - ) - .with_batch_config(self.batch_processor.clone().into()) - .build() - .filtered(), - )) + let batch_processor = opentelemetry::sdk::trace::BatchSpanProcessor::builder( + ExporterWrapper { + delegate: exporter, + span_metrics, + }, + opentelemetry::runtime::Tokio, + ) + .with_batch_config(self.batch_processor.clone().into()) + .build() + .filtered(); + + Ok( + if trace.preview_datadog_agent_sampling.unwrap_or_default() { + builder.with_span_processor(batch_processor.always_sampled()) + } else { + builder.with_span_processor(batch_processor) + }, + ) } } diff --git a/apollo-router/src/plugins/telemetry/tracing/datadog/span_processor.rs b/apollo-router/src/plugins/telemetry/tracing/datadog/span_processor.rs new file mode 100644 index 0000000000..7c879c310a --- /dev/null +++ b/apollo-router/src/plugins/telemetry/tracing/datadog/span_processor.rs @@ -0,0 +1,133 @@ +use opentelemetry_api::trace::SpanContext; +use opentelemetry_api::trace::TraceResult; +use opentelemetry_api::Context; +use opentelemetry_sdk::export::trace::SpanData; +use opentelemetry_sdk::trace::Span; +use opentelemetry_sdk::trace::SpanProcessor; + +/// When using the Datadog agent we need spans to always be exported. However, the batch span processor will only export spans that are sampled. +/// This wrapper will override the trace flags to always sample. +/// THe datadog exporter itself will look at the `sampling.priority` trace context attribute to determine if the span should be sampled. +#[derive(Debug)] +pub(crate) struct DatadogSpanProcessor { + delegate: T, +} + +impl DatadogSpanProcessor { + pub(crate) fn new(delegate: T) -> Self { + Self { delegate } + } +} + +impl SpanProcessor for DatadogSpanProcessor { + fn on_start(&self, span: &mut Span, cx: &Context) { + self.delegate.on_start(span, cx) + } + + fn on_end(&self, mut span: SpanData) { + // Note that the trace state for measuring and sampling priority is handled in the AgentSampler + // The only purpose of this span processor is to ensure that a span can pass through a batch processor. + let new_trace_flags = span.span_context.trace_flags().with_sampled(true); + span.span_context = SpanContext::new( + span.span_context.trace_id(), + span.span_context.span_id(), + new_trace_flags, + span.span_context.is_remote(), + span.span_context.trace_state().clone(), + ); + self.delegate.on_end(span) + } + + fn force_flush(&self) -> TraceResult<()> { + self.delegate.force_flush() + } + + fn shutdown(&mut self) -> TraceResult<()> { + self.delegate.shutdown() + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::sync::Mutex; + use std::time::SystemTime; + + use opentelemetry_api::trace::SpanId; + use opentelemetry_api::trace::SpanKind; + use opentelemetry_api::trace::TraceFlags; + use opentelemetry_api::trace::TraceId; + use opentelemetry_api::Context; + use opentelemetry_sdk::trace::EvictedHashMap; + use opentelemetry_sdk::trace::EvictedQueue; + use opentelemetry_sdk::trace::SpanProcessor; + + use super::*; + + #[derive(Debug, Clone)] + struct MockSpanProcessor { + spans: Arc>>, + } + + impl MockSpanProcessor { + fn new() -> Self { + Self { + spans: Default::default(), + } + } + } + + impl SpanProcessor for MockSpanProcessor { + fn on_start(&self, _span: &mut Span, _cx: &Context) {} + + fn on_end(&self, span: SpanData) { + self.spans.lock().unwrap().push(span); + } + + fn force_flush(&self) -> TraceResult<()> { + Ok(()) + } + + fn shutdown(&mut self) -> TraceResult<()> { + Ok(()) + } + } + + #[test] + fn test_on_end_updates_trace_flags() { + let mock_processor = MockSpanProcessor::new(); + let processor = DatadogSpanProcessor::new(mock_processor.clone()); + let span_context = SpanContext::new( + TraceId::from_u128(1), + SpanId::from_u64(1), + TraceFlags::default(), + false, + Default::default(), + ); + let span_data = SpanData { + span_context, + parent_span_id: SpanId::from_u64(1), + span_kind: SpanKind::Client, + name: Default::default(), + start_time: SystemTime::now(), + end_time: SystemTime::now(), + attributes: EvictedHashMap::new(32, 32), + events: EvictedQueue::new(32), + links: EvictedQueue::new(32), + status: Default::default(), + resource: Default::default(), + instrumentation_lib: Default::default(), + }; + + processor.on_end(span_data.clone()); + + // Verify that the trace flags are updated to sampled + let updated_trace_flags = span_data.span_context.trace_flags().with_sampled(true); + let stored_spans = mock_processor.spans.lock().unwrap(); + assert_eq!(stored_spans.len(), 1); + assert_eq!( + stored_spans[0].span_context.trace_flags(), + updated_trace_flags + ); + } +} diff --git a/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/model/v05.rs b/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/model/v05.rs index fd1590966e..e11bc9ed78 100644 --- a/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/model/v05.rs +++ b/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/model/v05.rs @@ -8,6 +8,7 @@ use super::unified_tags::UnifiedTags; use crate::plugins::telemetry::tracing::datadog_exporter::exporter::intern::StringInterner; use crate::plugins::telemetry::tracing::datadog_exporter::exporter::model::DD_MEASURED_KEY; use crate::plugins::telemetry::tracing::datadog_exporter::exporter::model::SAMPLING_PRIORITY_KEY; +use crate::plugins::telemetry::tracing::datadog_exporter::propagator::SamplingPriority; use crate::plugins::telemetry::tracing::datadog_exporter::DatadogTraceState; use crate::plugins::telemetry::tracing::datadog_exporter::Error; use crate::plugins::telemetry::tracing::datadog_exporter::ModelConfig; @@ -129,10 +130,22 @@ fn write_unified_tag<'a>( } fn get_sampling_priority(span: &SpanData) -> f64 { - if span.span_context.trace_state().priority_sampling_enabled() { - 1.0 - } else { - 0.0 + match span + .span_context + .trace_state() + .sampling_priority() + .unwrap_or_else(|| { + // Datadog sampling has not been set, revert to traceflags + if span.span_context.trace_flags().is_sampled() { + SamplingPriority::AutoKeep + } else { + SamplingPriority::AutoReject + } + }) { + SamplingPriority::UserReject => -1.0, + SamplingPriority::AutoReject => 0.0, + SamplingPriority::AutoKeep => 1.0, + SamplingPriority::UserKeep => 2.0, } } diff --git a/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/mod.rs b/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/mod.rs index 1c586d48c8..74907ee6a4 100644 --- a/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/mod.rs +++ b/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/mod.rs @@ -158,6 +158,8 @@ pub use propagator::DatadogTraceState; pub use propagator::DatadogTraceStateBuilder; pub(crate) mod propagator { + use std::fmt::Display; + use once_cell::sync::Lazy; use opentelemetry::propagation::text_map_propagator::FieldIter; use opentelemetry::propagation::Extractor; @@ -177,9 +179,9 @@ pub(crate) mod propagator { const TRACE_FLAG_DEFERRED: TraceFlags = TraceFlags::new(0x02); const TRACE_STATE_PRIORITY_SAMPLING: &str = "psr"; - pub(crate) const TRACE_STATE_MEASURE: &str = "m"; - pub(crate) const TRACE_STATE_TRUE_VALUE: &str = "1"; - pub(crate) const TRACE_STATE_FALSE_VALUE: &str = "0"; + const TRACE_STATE_MEASURE: &str = "m"; + const TRACE_STATE_TRUE_VALUE: &str = "1"; + const TRACE_STATE_FALSE_VALUE: &str = "0"; static DATADOG_HEADER_FIELDS: Lazy<[String; 3]> = Lazy::new(|| { [ @@ -191,8 +193,8 @@ pub(crate) mod propagator { #[derive(Default)] pub struct DatadogTraceStateBuilder { - priority_sampling: bool, - measuring: bool, + sampling_priority: SamplingPriority, + measuring: Option, } fn boolean_to_trace_state_flag(value: bool) -> &'static str { @@ -209,33 +211,39 @@ pub(crate) mod propagator { #[allow(clippy::needless_update)] impl DatadogTraceStateBuilder { - pub fn with_priority_sampling(self, enabled: bool) -> Self { + pub fn with_priority_sampling(self, sampling_priority: SamplingPriority) -> Self { Self { - priority_sampling: enabled, + sampling_priority, ..self } } pub fn with_measuring(self, enabled: bool) -> Self { Self { - measuring: enabled, + measuring: Some(enabled), ..self } } pub fn build(self) -> TraceState { - let values = [ - ( - TRACE_STATE_MEASURE, - boolean_to_trace_state_flag(self.measuring), - ), - ( + if let Some(measuring) = self.measuring { + let values = [ + (TRACE_STATE_MEASURE, boolean_to_trace_state_flag(measuring)), + ( + TRACE_STATE_PRIORITY_SAMPLING, + &self.sampling_priority.to_string(), + ), + ]; + + TraceState::from_key_value(values).unwrap_or_default() + } else { + let values = [( TRACE_STATE_PRIORITY_SAMPLING, - boolean_to_trace_state_flag(self.priority_sampling), - ), - ]; + &self.sampling_priority.to_string(), + )]; - TraceState::from_key_value(values).unwrap_or_default() + TraceState::from_key_value(values).unwrap_or_default() + } } } @@ -244,9 +252,9 @@ pub(crate) mod propagator { fn measuring_enabled(&self) -> bool; - fn with_priority_sampling(&self, enabled: bool) -> TraceState; + fn with_priority_sampling(&self, sampling_priority: SamplingPriority) -> TraceState; - fn priority_sampling_enabled(&self) -> bool; + fn sampling_priority(&self) -> Option; } impl DatadogTraceState for TraceState { @@ -261,30 +269,77 @@ pub(crate) mod propagator { .unwrap_or_default() } - fn with_priority_sampling(&self, enabled: bool) -> TraceState { - self.insert( - TRACE_STATE_PRIORITY_SAMPLING, - boolean_to_trace_state_flag(enabled), - ) - .unwrap_or_else(|_err| self.clone()) + fn with_priority_sampling(&self, sampling_priority: SamplingPriority) -> TraceState { + self.insert(TRACE_STATE_PRIORITY_SAMPLING, sampling_priority.to_string()) + .unwrap_or_else(|_err| self.clone()) } - fn priority_sampling_enabled(&self) -> bool { - self.get(TRACE_STATE_PRIORITY_SAMPLING) - .map(trace_flag_to_boolean) - .unwrap_or_default() + fn sampling_priority(&self) -> Option { + self.get(TRACE_STATE_PRIORITY_SAMPLING).map(|value| { + SamplingPriority::try_from(value).unwrap_or(SamplingPriority::AutoReject) + }) } } - enum SamplingPriority { + #[derive(Default, Debug, Eq, PartialEq)] + pub(crate) enum SamplingPriority { UserReject = -1, + #[default] AutoReject = 0, AutoKeep = 1, UserKeep = 2, } + impl SamplingPriority { + pub(crate) fn as_i64(&self) -> i64 { + match self { + SamplingPriority::UserReject => -1, + SamplingPriority::AutoReject => 0, + SamplingPriority::AutoKeep => 1, + SamplingPriority::UserKeep => 2, + } + } + } + + impl Display for SamplingPriority { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let value = match self { + SamplingPriority::UserReject => -1, + SamplingPriority::AutoReject => 0, + SamplingPriority::AutoKeep => 1, + SamplingPriority::UserKeep => 2, + }; + write!(f, "{}", value) + } + } + + impl SamplingPriority { + pub fn as_str(&self) -> &'static str { + match self { + SamplingPriority::UserReject => "-1", + SamplingPriority::AutoReject => "0", + SamplingPriority::AutoKeep => "1", + SamplingPriority::UserKeep => "2", + } + } + } + + impl TryFrom<&str> for SamplingPriority { + type Error = ExtractError; + + fn try_from(value: &str) -> Result { + match value { + "-1" => Ok(SamplingPriority::UserReject), + "0" => Ok(SamplingPriority::AutoReject), + "1" => Ok(SamplingPriority::AutoKeep), + "2" => Ok(SamplingPriority::UserKeep), + _ => Err(ExtractError::SamplingPriority), + } + } + } + #[derive(Debug)] - enum ExtractError { + pub(crate) enum ExtractError { TraceId, SpanId, SamplingPriority, @@ -311,16 +366,7 @@ pub(crate) mod propagator { } fn create_trace_state_and_flags(trace_flags: TraceFlags) -> (TraceState, TraceFlags) { - if trace_flags & TRACE_FLAG_DEFERRED == TRACE_FLAG_DEFERRED { - (TraceState::default(), trace_flags) - } else { - ( - DatadogTraceStateBuilder::default() - .with_priority_sampling(trace_flags.is_sampled()) - .build(), - TraceFlags::SAMPLED, - ) - } + (TraceState::default(), trace_flags) } impl DatadogPropagator { @@ -343,23 +389,6 @@ pub(crate) mod propagator { .map_err(|_| ExtractError::SpanId) } - fn extract_sampling_priority( - &self, - sampling_priority: &str, - ) -> Result { - let i = sampling_priority - .parse::() - .map_err(|_| ExtractError::SamplingPriority)?; - - match i { - -1 => Ok(SamplingPriority::UserReject), - 0 => Ok(SamplingPriority::AutoReject), - 1 => Ok(SamplingPriority::AutoKeep), - 2 => Ok(SamplingPriority::UserKeep), - _ => Err(ExtractError::SamplingPriority), - } - } - fn extract_span_context( &self, extractor: &dyn Extractor, @@ -371,11 +400,11 @@ pub(crate) mod propagator { let span_id = self .extract_span_id(extractor.get(DATADOG_PARENT_ID_HEADER).unwrap_or("")) .unwrap_or(SpanId::INVALID); - let sampling_priority = self.extract_sampling_priority( - extractor - .get(DATADOG_SAMPLING_PRIORITY_HEADER) - .unwrap_or(""), - ); + let sampling_priority = extractor + .get(DATADOG_SAMPLING_PRIORITY_HEADER) + .unwrap_or("") + .try_into(); + let sampled = match sampling_priority { Ok(SamplingPriority::UserReject) | Ok(SamplingPriority::AutoReject) => { TraceFlags::default() @@ -387,7 +416,10 @@ pub(crate) mod propagator { Err(_) => TRACE_FLAG_DEFERRED, }; - let (trace_state, trace_flags) = create_trace_state_and_flags(sampled); + let (mut trace_state, trace_flags) = create_trace_state_and_flags(sampled); + if let Ok(sampling_priority) = sampling_priority { + trace_state = trace_state.with_priority_sampling(sampling_priority); + } Ok(SpanContext::new( trace_id, @@ -399,14 +431,6 @@ pub(crate) mod propagator { } } - fn get_sampling_priority(span_context: &SpanContext) -> SamplingPriority { - if span_context.trace_state().priority_sampling_enabled() { - SamplingPriority::AutoKeep - } else { - SamplingPriority::AutoReject - } - } - impl TextMapPropagator for DatadogPropagator { fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) { let span = cx.span(); @@ -422,8 +446,11 @@ pub(crate) mod propagator { ); if span_context.trace_flags() & TRACE_FLAG_DEFERRED != TRACE_FLAG_DEFERRED { - let sampling_priority = get_sampling_priority(span_context); - + // The sampling priority + let sampling_priority = span_context + .trace_state() + .sampling_priority() + .unwrap_or_default(); injector.set( DATADOG_SAMPLING_PRIORITY_HEADER, (sampling_priority as i32).to_string(), @@ -460,8 +487,10 @@ pub(crate) mod propagator { (vec![(DATADOG_TRACE_ID_HEADER, "garbage")], SpanContext::empty_context()), (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "garbage")], SpanContext::new(TraceId::from_u128(1234), SpanId::INVALID, TRACE_FLAG_DEFERRED, true, TraceState::default())), (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TRACE_FLAG_DEFERRED, true, TraceState::default())), - (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "0")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::SAMPLED, true, DatadogTraceStateBuilder::default().with_priority_sampling(false).build())), - (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "1")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::SAMPLED, true, DatadogTraceStateBuilder::default().with_priority_sampling(true).build())), + (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "-1")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::default(), true, DatadogTraceStateBuilder::default().with_priority_sampling(SamplingPriority::UserReject).build())), + (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "0")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::default(), true, DatadogTraceStateBuilder::default().with_priority_sampling(SamplingPriority::AutoReject).build())), + (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "1")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::SAMPLED, true, DatadogTraceStateBuilder::default().with_priority_sampling(SamplingPriority::AutoKeep).build())), + (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "2")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::SAMPLED, true, DatadogTraceStateBuilder::default().with_priority_sampling(SamplingPriority::UserKeep).build())), ] } @@ -473,8 +502,10 @@ pub(crate) mod propagator { (vec![], SpanContext::new(TraceId::from_hex("1234").unwrap(), SpanId::INVALID, TRACE_FLAG_DEFERRED, true, TraceState::default())), (vec![], SpanContext::new(TraceId::from_hex("1234").unwrap(), SpanId::INVALID, TraceFlags::SAMPLED, true, TraceState::default())), (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TRACE_FLAG_DEFERRED, true, TraceState::default())), - (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "0")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::SAMPLED, true, DatadogTraceStateBuilder::default().with_priority_sampling(false).build())), - (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "1")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::SAMPLED, true, DatadogTraceStateBuilder::default().with_priority_sampling(true).build())), + (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "-1")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::default(), true, DatadogTraceStateBuilder::default().with_priority_sampling(SamplingPriority::UserReject).build())), + (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "0")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::default(), true, DatadogTraceStateBuilder::default().with_priority_sampling(SamplingPriority::AutoReject).build())), + (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "1")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::SAMPLED, true, DatadogTraceStateBuilder::default().with_priority_sampling(SamplingPriority::AutoKeep).build())), + (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "2")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::SAMPLED, true, DatadogTraceStateBuilder::default().with_priority_sampling(SamplingPriority::UserKeep).build())), ] } diff --git a/apollo-router/src/plugins/telemetry/tracing/mod.rs b/apollo-router/src/plugins/telemetry/tracing/mod.rs index 0172f3e094..d2dc62b138 100644 --- a/apollo-router/src/plugins/telemetry/tracing/mod.rs +++ b/apollo-router/src/plugins/telemetry/tracing/mod.rs @@ -18,6 +18,7 @@ use tower::BoxError; use super::config_new::spans::Spans; use super::formatters::APOLLO_PRIVATE_PREFIX; use crate::plugins::telemetry::config::TracingCommon; +use crate::plugins::telemetry::tracing::datadog::DatadogSpanProcessor; pub(crate) mod apollo; pub(crate) mod apollo_telemetry; @@ -91,6 +92,7 @@ where Self: Sized + SpanProcessor, { fn filtered(self) -> ApolloFilterSpanProcessor; + fn always_sampled(self) -> DatadogSpanProcessor; } impl SpanProcessorExt for T @@ -100,6 +102,12 @@ where fn filtered(self) -> ApolloFilterSpanProcessor { ApolloFilterSpanProcessor { delegate: self } } + + /// This span processor will always send spans to the exporter even if they are not sampled. This is useful for the datadog agent which + /// uses spans for metrics. + fn always_sampled(self) -> DatadogSpanProcessor { + DatadogSpanProcessor::new(self) + } } /// Batch processor configuration diff --git a/apollo-router/src/plugins/telemetry/tracing/otlp.rs b/apollo-router/src/plugins/telemetry/tracing/otlp.rs index be294427f2..9a61075e5f 100644 --- a/apollo-router/src/plugins/telemetry/tracing/otlp.rs +++ b/apollo-router/src/plugins/telemetry/tracing/otlp.rs @@ -20,20 +20,23 @@ impl TracingConfigurator for super::super::otlp::Config { fn apply( &self, builder: Builder, - _common: &TracingCommon, + common: &TracingCommon, _spans_config: &Spans, ) -> Result { - tracing::info!("Configuring Otlp tracing: {}", self.batch_processor); let exporter: SpanExporterBuilder = self.exporter(TelemetryDataKind::Traces)?; - - Ok(builder.with_span_processor( - BatchSpanProcessor::builder( - exporter.build_span_exporter()?, - opentelemetry::runtime::Tokio, - ) - .with_batch_config(self.batch_processor.clone().into()) - .build() - .filtered(), - )) + let batch_span_processor = BatchSpanProcessor::builder( + exporter.build_span_exporter()?, + opentelemetry::runtime::Tokio, + ) + .with_batch_config(self.batch_processor.clone().into()) + .build() + .filtered(); + Ok( + if common.preview_datadog_agent_sampling.unwrap_or_default() { + builder.with_span_processor(batch_span_processor.always_sampled()) + } else { + builder.with_span_processor(batch_span_processor) + }, + ) } } diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index 826a377e04..3c222ba3d6 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -4,6 +4,7 @@ use std::net::SocketAddr; use std::net::TcpListener; use std::path::PathBuf; use std::process::Stdio; +use std::str::FromStr; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; @@ -18,6 +19,7 @@ use fred::types::Scanner; use futures::StreamExt; use http::header::ACCEPT; use http::header::CONTENT_TYPE; +use http::HeaderName; use http::HeaderValue; use mediatype::names::BOUNDARY; use mediatype::names::FORM_DATA; @@ -33,6 +35,7 @@ use opentelemetry::sdk::trace::TracerProvider; use opentelemetry::sdk::Resource; use opentelemetry::testing::trace::NoopSpanExporter; use opentelemetry::trace::TraceContextExt; +use opentelemetry_api::trace::SpanContext; use opentelemetry_api::trace::TraceId; use opentelemetry_api::trace::TracerProvider as OtherTracerProvider; use opentelemetry_api::Context; @@ -126,7 +129,7 @@ impl Respond for TracedResponder { pub enum Telemetry { Jaeger, Otlp { - endpoint: String, + endpoint: Option, }, Datadog, Zipkin, @@ -156,7 +159,9 @@ impl Telemetry { .build(), ) .build(), - Telemetry::Otlp { endpoint } => TracerProvider::builder() + Telemetry::Otlp { + endpoint: Some(endpoint), + } => TracerProvider::builder() .with_config(config) .with_span_processor( BatchSpanProcessor::builder( @@ -201,7 +206,7 @@ impl Telemetry { .build(), ) .build(), - Telemetry::None => TracerProvider::builder() + Telemetry::None | Telemetry::Otlp { endpoint: None } => TracerProvider::builder() .with_config(config) .with_simple_exporter(NoopSpanExporter::default()) .build(), @@ -258,7 +263,29 @@ impl Telemetry { } Telemetry::Datadog => { let propagator = opentelemetry_datadog::DatadogPropagator::new(); - propagator.extract(&headers) + let mut context = propagator.extract(&headers); + // We're going to override the sampled so that we can test sampling priority + if let Some(psr) = headers.get("x-datadog-sampling-priority") { + let state = context + .span() + .span_context() + .trace_state() + .insert("psr", psr.to_string()) + .expect("psr"); + context = context.with_remote_span_context(SpanContext::new( + context.span().span_context().trace_id(), + context.span().span_context().span_id(), + context + .span() + .span_context() + .trace_flags() + .with_sampled(true), + true, + state, + )); + } + + context } Telemetry::Otlp { .. } => { let propagator = opentelemetry::sdk::propagation::TraceContextPropagator::default(); @@ -568,7 +595,7 @@ impl IntegrationTest { async move { let client = reqwest::Client::new(); - let mut builder = client + let builder = client .post(url) .header( CONTENT_TYPE, @@ -579,14 +606,19 @@ impl IntegrationTest { .header("x-my-header", "test") .header("head", "test"); + let mut request = builder.json(&query).build().unwrap(); + telemetry.inject_context(&mut request); + if let Some(headers) = headers { for (name, value) in headers { - builder = builder.header(name, value); + request.headers_mut().remove(&name); + request.headers_mut().append( + HeaderName::from_str(&name).expect("header was invalid"), + value.try_into().expect("header was invalid"), + ); } } - let mut request = builder.json(&query).build().unwrap(); - telemetry.inject_context(&mut request); request.headers_mut().remove(ACCEPT); match client.execute(request).await { Ok(response) => (span_id, response), @@ -605,6 +637,7 @@ impl IntegrationTest { pub fn execute_untraced_query( &self, query: &Value, + headers: Option>, ) -> impl std::future::Future { assert!( self.router.is_some(), @@ -626,6 +659,16 @@ impl IntegrationTest { .unwrap(); request.headers_mut().remove(ACCEPT); + if let Some(headers) = headers { + for (name, value) in headers { + request.headers_mut().remove(&name); + request.headers_mut().append( + HeaderName::from_str(&name).expect("header was invalid"), + value.try_into().expect("header was invalid"), + ); + } + } + match client.execute(request).await { Ok(response) => ( TraceId::from_hex( diff --git a/apollo-router/tests/integration/mod.rs b/apollo-router/tests/integration/mod.rs index c383b5348f..06b77f688f 100644 --- a/apollo-router/tests/integration/mod.rs +++ b/apollo-router/tests/integration/mod.rs @@ -39,3 +39,12 @@ impl ValueExt for Value { self.as_str().map(|s| s.to_string()) } } + +impl ValueExt for &Value { + fn select_path<'a>(&'a self, path: &str) -> Result, BoxError> { + Ok(Selector::new().str_path(path)?.value(self).select()?) + } + fn as_string(&self) -> Option { + self.as_str().map(|s| s.to_string()) + } +} diff --git a/apollo-router/tests/integration/telemetry/datadog.rs b/apollo-router/tests/integration/telemetry/datadog.rs index 6aed76ff6d..39757ee389 100644 --- a/apollo-router/tests/integration/telemetry/datadog.rs +++ b/apollo-router/tests/integration/telemetry/datadog.rs @@ -2,17 +2,18 @@ extern crate core; use std::collections::HashMap; use std::collections::HashSet; -use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use std::sync::Mutex; use std::time::Duration; use anyhow::anyhow; +use opentelemetry_api::trace::SpanContext; use opentelemetry_api::trace::TraceContextExt; use opentelemetry_api::trace::TraceId; +use opentelemetry_api::Context; use serde_json::json; use serde_json::Value; use tower::BoxError; -use tracing::Span; -use tracing_opentelemetry::OpenTelemetrySpanExt; use wiremock::ResponseTemplate; use crate::integration::common::graph_os_enabled; @@ -28,6 +29,9 @@ struct TraceSpec { span_names: HashSet<&'static str>, measured_spans: HashSet<&'static str>, unmeasured_spans: HashSet<&'static str>, + priority_sampled: Option<&'static str>, + // Not the metrics but the otel attribute + no_priority_sampled_attribute: Option, } #[tokio::test(flavor = "multi_thread")] @@ -35,8 +39,8 @@ async fn test_no_sample() -> Result<(), BoxError> { if !graph_os_enabled() { return Ok(()); } - let subgraph_was_sampled = std::sync::Arc::new(AtomicBool::new(false)); - let subgraph_was_sampled_callback = subgraph_was_sampled.clone(); + let context = std::sync::Arc::new(std::sync::Mutex::new(None)); + let context_clone = context.clone(); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Datadog) .config(include_str!("fixtures/datadog_no_sample.router.yaml")) @@ -44,8 +48,10 @@ async fn test_no_sample() -> Result<(), BoxError> { json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), )) .subgraph_callback(Box::new(move || { - let sampled = Span::current().context().span().span_context().is_sampled(); - subgraph_was_sampled_callback.store(sampled, std::sync::atomic::Ordering::SeqCst); + let context = Context::current(); + let span = context.span(); + let span_context = span.span_context(); + *context_clone.lock().expect("poisoned") = Some(span_context.clone()); })) .build() .await; @@ -54,14 +60,318 @@ async fn test_no_sample() -> Result<(), BoxError> { router.assert_started().await; let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (_id, result) = router.execute_untraced_query(&query).await; + let (_id, result) = router.execute_untraced_query(&query, None).await; router.graceful_shutdown().await; assert!(result.status().is_success()); - assert!(!subgraph_was_sampled.load(std::sync::atomic::Ordering::SeqCst)); + let context = context + .lock() + .expect("poisoned") + .as_ref() + .expect("state") + .clone(); + assert!(context.is_sampled()); + assert_eq!(context.trace_state().get("psr"), Some("0")); Ok(()) } +// We want to check we're able to override the behavior of preview_datadog_agent_sampling configuration even if we set a datadog exporter +#[tokio::test(flavor = "multi_thread")] +async fn test_sampling_datadog_agent_disabled() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + let context = std::sync::Arc::new(std::sync::Mutex::new(None)); + let context_clone = context.clone(); + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Datadog) + .config(include_str!( + "fixtures/datadog_agent_sampling_disabled.router.yaml" + )) + .responder(ResponseTemplate::new(200).set_body_json( + json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), + )) + .subgraph_callback(Box::new(move || { + let context = Context::current(); + let span = context.span(); + let span_context = span.span_context(); + *context_clone.lock().expect("poisoned") = Some(span_context.clone()); + })) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); + let (id, result) = router.execute_untraced_query(&query, None).await; + router.graceful_shutdown().await; + assert!(result.status().is_success()); + let _context = context + .lock() + .expect("poisoned") + .as_ref() + .expect("state") + .clone(); + + tokio::time::sleep(Duration::from_secs(2)).await; + TraceSpec::builder() + .services([].into()) + .build() + .validate_trace(id) + .await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_priority_sampling_propagated() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + let context = std::sync::Arc::new(std::sync::Mutex::new(None)); + let context_clone = context.clone(); + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Datadog) + .config(include_str!("fixtures/datadog.router.yaml")) + .responder(ResponseTemplate::new(200).set_body_json( + json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), + )) + .subgraph_callback(Box::new(move || { + let context = Context::current(); + let span = context.span(); + let span_context = span.span_context(); + *context_clone.lock().expect("poisoned") = Some(span_context.clone()); + })) + .build() + .await; + + router.start().await; + router.assert_started().await; + + // Parent based sampling. psr MUST be populated with the value that we pass in. + test_psr( + &context, + &mut router, + Some("-1"), + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("-1") + .build(), + ) + .await?; + test_psr( + &context, + &mut router, + Some("0"), + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("0") + .build(), + ) + .await?; + test_psr( + &context, + &mut router, + Some("1"), + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .build(), + ) + .await?; + test_psr( + &context, + &mut router, + Some("2"), + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("2") + .build(), + ) + .await?; + + // No psr was passed in the router is free to set it. This will be 1 as we are going to sample here. + test_psr( + &context, + &mut router, + None, + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .build(), + ) + .await?; + + router.graceful_shutdown().await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_priority_sampling_propagated_otel_request() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + let context = std::sync::Arc::new(std::sync::Mutex::new(None)); + let context_clone = context.clone(); + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Otlp { endpoint: None }) + .config(include_str!("fixtures/datadog.router.yaml")) + .responder(ResponseTemplate::new(200).set_body_json( + json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), + )) + .subgraph_callback(Box::new(move || { + let context = Context::current(); + let span = context.span(); + let span_context = span.span_context(); + *context_clone.lock().expect("poisoned") = Some(span_context.clone()); + })) + .build() + .await; + + router.start().await; + router.assert_started().await; + let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); + let (id, result) = router.execute_query(&query).await; + assert_eq!( + result + .headers() + .get("apollo-custom-trace-id") + .unwrap() + .to_str() + .unwrap(), + id.to_datadog() + ); + TraceSpec::builder() + .services(["router"].into()) + .priority_sampled("1") + .build() + .validate_trace(id) + .await?; + + router.graceful_shutdown().await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + let context = std::sync::Arc::new(std::sync::Mutex::new(None)); + let context_clone = context.clone(); + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Datadog) + .config(include_str!( + "fixtures/datadog_no_parent_sampler.router.yaml" + )) + .responder(ResponseTemplate::new(200).set_body_json( + json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), + )) + .subgraph_callback(Box::new(move || { + let context = Context::current(); + let span = context.span(); + let span_context = span.span_context(); + *context_clone.lock().expect("poisoned") = Some(span_context.clone()); + })) + .build() + .await; + + router.start().await; + router.assert_started().await; + + // The router will ignore the upstream PSR as parent based sampling is disabled. + test_psr( + &context, + &mut router, + Some("-1"), + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .build(), + ) + .await?; + test_psr( + &context, + &mut router, + Some("0"), + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .build(), + ) + .await?; + test_psr( + &context, + &mut router, + Some("1"), + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .build(), + ) + .await?; + test_psr( + &context, + &mut router, + Some("2"), + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .build(), + ) + .await?; + + test_psr( + &context, + &mut router, + None, + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .build(), + ) + .await?; + + router.graceful_shutdown().await; + + Ok(()) +} + +async fn test_psr( + context: &Arc>>, + router: &mut IntegrationTest, + psr: Option<&str>, + trace_spec: TraceSpec, +) -> Result<(), BoxError> { + let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); + let headers = if let Some(psr) = psr { + vec![("x-datadog-sampling-priority".to_string(), psr.to_string())] + } else { + vec![] + }; + let (id, result) = router + .execute_query_with_headers(&query, headers.into_iter().collect()) + .await; + + assert!(result.status().is_success()); + let context = context + .lock() + .expect("poisoned") + .as_ref() + .expect("state") + .clone(); + + assert_eq!( + context.trace_state().get("psr"), + trace_spec.priority_sampled + ); + trace_spec.validate_trace(id).await?; + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn test_default_span_names() -> Result<(), BoxError> { if !graph_os_enabled() { @@ -506,7 +816,7 @@ impl TraceSpec { async fn validate_trace(&self, id: TraceId) -> Result<(), BoxError> { let datadog_id = id.to_datadog(); let url = format!("http://localhost:8126/test/traces?trace_ids={datadog_id}"); - for _ in 0..10 { + for _ in 0..20 { if self.find_valid_trace(&url).await.is_ok() { return Ok(()); } @@ -533,11 +843,12 @@ impl TraceSpec { tracing::debug!("{}", serde_json::to_string_pretty(&trace)?); self.verify_trace_participants(&trace)?; self.verify_spans_present(&trace)?; - self.validate_measured_spans(&trace)?; + self.verify_measured_spans(&trace)?; self.verify_operation_name(&trace)?; self.verify_priority_sampled(&trace)?; + self.verify_priority_sampled_attribute(&trace)?; self.verify_version(&trace)?; - self.validate_span_kinds(&trace)?; + self.verify_span_kinds(&trace)?; Ok(()) } @@ -556,7 +867,7 @@ impl TraceSpec { Ok(()) } - fn validate_measured_spans(&self, trace: &Value) -> Result<(), BoxError> { + fn verify_measured_spans(&self, trace: &Value) -> Result<(), BoxError> { for expected in &self.measured_spans { assert!( self.measured_span(trace, expected)?, @@ -591,11 +902,13 @@ impl TraceSpec { .unwrap_or_default()) } - fn validate_span_kinds(&self, trace: &Value) -> Result<(), BoxError> { + fn verify_span_kinds(&self, trace: &Value) -> Result<(), BoxError> { // Validate that the span.kind has been propagated. We can just do this for a selection of spans. - self.validate_span_kind(trace, "router", "server")?; - self.validate_span_kind(trace, "supergraph", "internal")?; - self.validate_span_kind(trace, "http_request", "client")?; + if self.services.contains("router") { + self.validate_span_kind(trace, "router", "server")?; + self.validate_span_kind(trace, "supergraph", "internal")?; + self.validate_span_kind(trace, "http_request", "client")?; + } Ok(()) } @@ -652,19 +965,24 @@ impl TraceSpec { trace.select_path(&format!("$..[?(@.name == '{}')].meta.['span.kind']", name))?; let binding = binding1.first().or(binding2.first()); - assert!( - binding.is_some(), - "span.kind missing or incorrect {}, {}", - name, - trace - ); - assert_eq!( - binding - .expect("expected binding") - .as_str() - .expect("expected string"), - kind - ); + if binding.is_none() { + return Err(BoxError::from(format!( + "span.kind missing or incorrect {}, {}", + name, trace + ))); + } + + let binding = binding + .expect("expected binding") + .as_str() + .expect("expected string"); + if binding != kind { + return Err(BoxError::from(format!( + "span.kind mismatch, expected {} got {}", + kind, binding + ))); + } + Ok(()) } @@ -685,17 +1003,35 @@ impl TraceSpec { } fn verify_priority_sampled(&self, trace: &Value) -> Result<(), BoxError> { - let binding = trace.select_path("$.._sampling_priority_v1")?; - let sampling_priority = binding.first(); - // having this priority set to 1.0 everytime is not a problem as we're doing pre sampling in the full telemetry stack - // So basically if the trace was not sampled it wouldn't get to this stage and so nothing would be sent - assert_eq!( - sampling_priority - .expect("sampling priority expected") - .as_f64() - .expect("sampling priority must be a number"), - 1.0 - ); + if let Some(psr) = self.priority_sampled { + let binding = + trace.select_path("$..[?(@.service=='router')].metrics._sampling_priority_v1")?; + if binding.is_empty() { + return Err(BoxError::from("missing sampling priority")); + } + for sampling_priority in binding { + assert_eq!( + sampling_priority + .as_f64() + .expect("psr not string") + .to_string(), + psr + ); + } + } + Ok(()) + } + + fn verify_priority_sampled_attribute(&self, trace: &Value) -> Result<(), BoxError> { + if self.no_priority_sampled_attribute.unwrap_or_default() { + let binding = + trace.select_path("$..[?(@.service=='router')].meta['sampling.priority']")?; + if binding.is_empty() { + return Ok(()); + } else { + return Err(BoxError::from("sampling priority attribute exists")); + } + } Ok(()) } } diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog.router.yaml index d6ecc66607..0f0f50dd78 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/datadog.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog.router.yaml @@ -13,6 +13,7 @@ telemetry: resource: env: local1 service.version: router_version_override + preview_datadog_agent_sampling: true datadog: enabled: true batch_processor: diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_agent_sampling_disabled.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_agent_sampling_disabled.router.yaml new file mode 100644 index 0000000000..49b1528c94 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_agent_sampling_disabled.router.yaml @@ -0,0 +1,23 @@ +telemetry: + apollo: + field_level_instrumentation_sampler: always_off + exporters: + tracing: + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + common: + service_name: router + # NOT always_off to allow us to test a sampling probability of zero + sampler: 0.0 + preview_datadog_agent_sampling: false + datadog: + enabled: true + batch_processor: + scheduled_delay: 100ms + fixed_span_names: false + enable_span_mapping: false + instrumentation: + spans: + mode: spec_compliant + diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_default_span_names.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_default_span_names.router.yaml index 67c2c070e6..e874c00fab 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/datadog_default_span_names.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_default_span_names.router.yaml @@ -7,6 +7,7 @@ telemetry: format: datadog common: service_name: router + preview_datadog_agent_sampling: true datadog: enabled: true batch_processor: diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_no_parent_sampler.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_no_parent_sampler.router.yaml new file mode 100644 index 0000000000..2e9c634dd9 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_no_parent_sampler.router.yaml @@ -0,0 +1,28 @@ +telemetry: + exporters: + tracing: + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + format: datadog + propagation: + trace_context: true + jaeger: true + common: + service_name: router + parent_based_sampler: false + resource: + env: local1 + service.version: router_version_override + preview_datadog_agent_sampling: true + datadog: + enabled: true + batch_processor: + scheduled_delay: 100ms + instrumentation: + spans: + mode: spec_compliant + supergraph: + attributes: + graphql.operation.name: true + diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_no_sample.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_no_sample.router.yaml index d89d104346..19af041c56 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/datadog_no_sample.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_no_sample.router.yaml @@ -11,6 +11,7 @@ telemetry: service_name: router # NOT always_off to allow us to test a sampling probability of zero sampler: 0.0 + preview_datadog_agent_sampling: true datadog: enabled: true batch_processor: diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_override_span_names.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_override_span_names.router.yaml index 7d5e1ff2e1..bb793301d0 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/datadog_override_span_names.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_override_span_names.router.yaml @@ -7,6 +7,7 @@ telemetry: format: datadog common: service_name: router + preview_datadog_agent_sampling: true datadog: enabled: true # Span mapping will always override the span name as far as the test agent is concerned diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_override_span_names_late.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_override_span_names_late.router.yaml index dda383a784..821662b5be 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/datadog_override_span_names_late.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_override_span_names_late.router.yaml @@ -7,6 +7,7 @@ telemetry: format: datadog common: service_name: router + preview_datadog_agent_sampling: true datadog: enabled: true # Span mapping will always override the span name as far as the test agent is concerned diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_resource_mapping_default.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_resource_mapping_default.router.yaml index 96160b1831..0603e72c9c 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/datadog_resource_mapping_default.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_resource_mapping_default.router.yaml @@ -7,6 +7,7 @@ telemetry: format: datadog common: service_name: router + preview_datadog_agent_sampling: true datadog: enabled: true enable_span_mapping: true diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_resource_mapping_override.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_resource_mapping_override.router.yaml index a01c44fc61..5eba22068b 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/datadog_resource_mapping_override.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_resource_mapping_override.router.yaml @@ -7,6 +7,7 @@ telemetry: format: datadog common: service_name: router + preview_datadog_agent_sampling: true datadog: enabled: true enable_span_mapping: true diff --git a/apollo-router/tests/integration/telemetry/fixtures/otlp.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/otlp.router.yaml index f4484786f4..aa56c66187 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/otlp.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/otlp.router.yaml @@ -9,7 +9,7 @@ telemetry: otlp: enabled: true protocol: http - endpoint: /traces + endpoint: batch_processor: scheduled_delay: 10ms metrics: @@ -22,3 +22,15 @@ telemetry: batch_processor: scheduled_delay: 10ms + + instrumentation: + spans: + mode: spec_compliant + supergraph: + attributes: + graphql.operation.name: true + + subgraph: + attributes: + otel.name: + subgraph_operation_name: string \ No newline at end of file diff --git a/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_agent_no_sample.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_agent_no_sample.router.yaml new file mode 100644 index 0000000000..77529f500d --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_agent_no_sample.router.yaml @@ -0,0 +1,42 @@ +telemetry: + apollo: + field_level_instrumentation_sampler: always_off + exporters: + tracing: + propagation: + datadog: true + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + common: + service_name: router + preview_datadog_agent_sampling: true + sampler: 0.0 + otlp: + enabled: true + protocol: http + endpoint: + batch_processor: + scheduled_delay: 10ms + metrics: + common: + service_name: router + otlp: + enabled: true + endpoint: /metrics + protocol: http + batch_processor: + scheduled_delay: 10ms + + + instrumentation: + spans: + mode: spec_compliant + supergraph: + attributes: + graphql.operation.name: true + + subgraph: + attributes: + otel.name: + subgraph_operation_name: string \ No newline at end of file diff --git a/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_agent_sample.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_agent_sample.router.yaml new file mode 100644 index 0000000000..6b1f32f71f --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_agent_sample.router.yaml @@ -0,0 +1,42 @@ +telemetry: + apollo: + field_level_instrumentation_sampler: always_off + exporters: + tracing: + propagation: + datadog: true + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + common: + service_name: router + preview_datadog_agent_sampling: true + sampler: 1.0 + otlp: + enabled: true + protocol: http + endpoint: + batch_processor: + scheduled_delay: 10ms + metrics: + common: + service_name: router + otlp: + enabled: true + endpoint: /metrics + protocol: http + batch_processor: + scheduled_delay: 10ms + + + instrumentation: + spans: + mode: spec_compliant + supergraph: + attributes: + graphql.operation.name: true + + subgraph: + attributes: + otel.name: + subgraph_operation_name: string \ No newline at end of file diff --git a/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_agent_sample_no_sample.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_agent_sample_no_sample.router.yaml new file mode 100644 index 0000000000..77529f500d --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_agent_sample_no_sample.router.yaml @@ -0,0 +1,42 @@ +telemetry: + apollo: + field_level_instrumentation_sampler: always_off + exporters: + tracing: + propagation: + datadog: true + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + common: + service_name: router + preview_datadog_agent_sampling: true + sampler: 0.0 + otlp: + enabled: true + protocol: http + endpoint: + batch_processor: + scheduled_delay: 10ms + metrics: + common: + service_name: router + otlp: + enabled: true + endpoint: /metrics + protocol: http + batch_processor: + scheduled_delay: 10ms + + + instrumentation: + spans: + mode: spec_compliant + supergraph: + attributes: + graphql.operation.name: true + + subgraph: + attributes: + otel.name: + subgraph_operation_name: string \ No newline at end of file diff --git a/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_propagation.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_propagation.router.yaml new file mode 100644 index 0000000000..7352f3d620 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_propagation.router.yaml @@ -0,0 +1,39 @@ +telemetry: + exporters: + tracing: + propagation: + datadog: true + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + common: + service_name: router + preview_datadog_agent_sampling: true + otlp: + enabled: true + protocol: http + endpoint: + batch_processor: + scheduled_delay: 10ms + metrics: + common: + service_name: router + otlp: + enabled: true + endpoint: /metrics + protocol: http + batch_processor: + scheduled_delay: 10ms + + + instrumentation: + spans: + mode: spec_compliant + supergraph: + attributes: + graphql.operation.name: true + + subgraph: + attributes: + otel.name: + subgraph_operation_name: string \ No newline at end of file diff --git a/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_propagation_no_agent.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_propagation_no_agent.router.yaml new file mode 100644 index 0000000000..08323073f3 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_propagation_no_agent.router.yaml @@ -0,0 +1,38 @@ +telemetry: + exporters: + tracing: + propagation: + datadog: true + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + common: + service_name: router + otlp: + enabled: true + protocol: http + endpoint: + batch_processor: + scheduled_delay: 10ms + metrics: + common: + service_name: router + otlp: + enabled: true + endpoint: /metrics + protocol: http + batch_processor: + scheduled_delay: 10ms + + + instrumentation: + spans: + mode: spec_compliant + supergraph: + attributes: + graphql.operation.name: true + + subgraph: + attributes: + otel.name: + subgraph_operation_name: string \ No newline at end of file diff --git a/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_propagation_no_parent_sampler.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_propagation_no_parent_sampler.router.yaml new file mode 100644 index 0000000000..7fd47f096b --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_propagation_no_parent_sampler.router.yaml @@ -0,0 +1,40 @@ +telemetry: + exporters: + tracing: + propagation: + datadog: true + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + common: + parent_based_sampler: false + preview_datadog_agent_sampling: true + service_name: router + otlp: + enabled: true + protocol: http + endpoint: + batch_processor: + scheduled_delay: 10ms + metrics: + common: + service_name: router + otlp: + enabled: true + endpoint: /metrics + protocol: http + batch_processor: + scheduled_delay: 10ms + + + instrumentation: + spans: + mode: spec_compliant + supergraph: + attributes: + graphql.operation.name: true + + subgraph: + attributes: + otel.name: + subgraph_operation_name: string \ No newline at end of file diff --git a/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_request_with_zipkin_propagator.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_request_with_zipkin_propagator.router.yaml new file mode 100644 index 0000000000..4e31e0d1d6 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_request_with_zipkin_propagator.router.yaml @@ -0,0 +1,40 @@ +telemetry: + apollo: + field_level_instrumentation_sampler: always_off + exporters: + tracing: + propagation: + zipkin: true + trace_context: true + common: + service_name: router + preview_datadog_agent_sampling: true + sampler: 1.0 + otlp: + enabled: true + protocol: http + endpoint: + batch_processor: + scheduled_delay: 10ms + metrics: + common: + service_name: router + otlp: + enabled: true + endpoint: /metrics + protocol: http + batch_processor: + scheduled_delay: 10ms + + + instrumentation: + spans: + mode: spec_compliant + supergraph: + attributes: + graphql.operation.name: true + + subgraph: + attributes: + otel.name: + subgraph_operation_name: string \ No newline at end of file diff --git a/apollo-router/tests/integration/telemetry/fixtures/otlp_no_parent_sampler.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/otlp_no_parent_sampler.router.yaml new file mode 100644 index 0000000000..5fdf22e0d6 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/otlp_no_parent_sampler.router.yaml @@ -0,0 +1,25 @@ +telemetry: + exporters: + tracing: + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + common: + service_name: router + parent_based_sampler: false + otlp: + enabled: true + protocol: http + endpoint: /traces + batch_processor: + scheduled_delay: 10ms + metrics: + common: + service_name: router + otlp: + enabled: true + endpoint: /metrics + protocol: http + batch_processor: + scheduled_delay: 10ms + diff --git a/apollo-router/tests/integration/telemetry/jaeger.rs b/apollo-router/tests/integration/telemetry/jaeger.rs index fcf59e4ef5..c9e79bd22a 100644 --- a/apollo-router/tests/integration/telemetry/jaeger.rs +++ b/apollo-router/tests/integration/telemetry/jaeger.rs @@ -90,7 +90,7 @@ async fn test_local_root() -> Result<(), BoxError> { router.assert_started().await; let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_untraced_query(&query).await; + let (id, result) = router.execute_untraced_query(&query, None).await; assert!(!result .headers() .get("apollo-custom-trace-id") @@ -121,7 +121,7 @@ async fn test_local_root_no_sample() -> Result<(), BoxError> { router.assert_started().await; let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (_, response) = router.execute_untraced_query(&query).await; + let (_, response) = router.execute_untraced_query(&query, None).await; assert!(response.headers().get("apollo-custom-trace-id").is_some()); router.graceful_shutdown().await; @@ -141,7 +141,7 @@ async fn test_local_root_50_percent_sample() -> Result<(), BoxError> { let query = json!({"query":"query ExampleQuery {topProducts{name}}\n","variables":{}, "operationName": "ExampleQuery"}); for _ in 0..100 { - let (id, result) = router.execute_untraced_query(&query).await; + let (id, result) = router.execute_untraced_query(&query, None).await; if result.headers().get("apollo-custom-trace-id").is_some() && validate_trace( @@ -177,7 +177,7 @@ async fn test_no_telemetry() -> Result<(), BoxError> { router.assert_started().await; let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (_, response) = router.execute_untraced_query(&query).await; + let (_, response) = router.execute_untraced_query(&query, None).await; assert!(response.headers().get("apollo-custom-trace-id").is_none()); router.graceful_shutdown().await; diff --git a/apollo-router/tests/integration/telemetry/otlp.rs b/apollo-router/tests/integration/telemetry/otlp.rs index 7eae04f567..0ba9178cec 100644 --- a/apollo-router/tests/integration/telemetry/otlp.rs +++ b/apollo-router/tests/integration/telemetry/otlp.rs @@ -1,10 +1,10 @@ extern crate core; +use std::collections::HashMap; use std::collections::HashSet; use std::time::Duration; use anyhow::anyhow; -use itertools::Itertools; use opentelemetry_api::trace::TraceId; use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceResponse; use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceResponse; @@ -18,37 +18,22 @@ use wiremock::Mock; use wiremock::MockServer; use wiremock::ResponseTemplate; +use crate::integration::common::graph_os_enabled; use crate::integration::common::Telemetry; use crate::integration::IntegrationTest; use crate::integration::ValueExt; #[tokio::test(flavor = "multi_thread")] async fn test_basic() -> Result<(), BoxError> { - let mock_server = wiremock::MockServer::start().await; - Mock::given(method("POST")) - .and(path("/traces")) - .respond_with(ResponseTemplate::new(200).set_body_raw( - ExportTraceServiceResponse::default().encode_to_vec(), - "application/x-protobuf", - )) - .expect(1..) - .mount(&mock_server) - .await; - Mock::given(method("POST")) - .and(path("/metrics")) - .respond_with(ResponseTemplate::new(200).set_body_raw( - ExportMetricsServiceResponse::default().encode_to_vec(), - "application/x-protobuf", - )) - .expect(1..) - .mount(&mock_server) - .await; - + if !graph_os_enabled() { + panic!("Error: test skipped because GraphOS is not enabled"); + } + let mock_server = mock_otlp_server().await; let config = include_str!("fixtures/otlp.router.yaml") .replace("", &mock_server.uri()); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Otlp { - endpoint: format!("{}/traces", mock_server.uri()), + endpoint: Some(format!("{}/v1/traces", mock_server.uri())), }) .config(&config) .build() @@ -65,15 +50,31 @@ async fn test_basic() -> Result<(), BoxError> { .get("apollo-custom-trace-id") .unwrap() .is_empty()); - validate_telemetry( - &mock_server, - id, - &query, - Some("ExampleQuery"), - &["client", "router", "subgraph"], - false, - ) - .await?; + Spec::builder() + .operation_name("ExampleQuery") + .services(["client", "router", "subgraph"].into()) + .span_names( + [ + "query_planning", + "client_request", + "ExampleQuery__products__0", + "fetch", + "execution", + "query ExampleQuery", + "subgraph server", + "parse_query", + "http_request", + ] + .into(), + ) + .build() + .validate_trace(id, &mock_server) + .await?; + Spec::builder() + .service("router") + .build() + .validate_metrics(&mock_server) + .await?; router.touch_config().await; router.assert_reloaded().await; } @@ -81,146 +82,745 @@ async fn test_basic() -> Result<(), BoxError> { Ok(()) } -async fn validate_telemetry( - mock_server: &MockServer, - _id: TraceId, - query: &Value, - operation_name: Option<&str>, - services: &[&'static str], - custom_span_instrumentation: bool, +#[tokio::test(flavor = "multi_thread")] +async fn test_otlp_request_with_datadog_propagator() -> Result<(), BoxError> { + if !graph_os_enabled() { + panic!("Error: test skipped because GraphOS is not enabled"); + } + let mock_server = mock_otlp_server().await; + let config = include_str!("fixtures/otlp_datadog_propagation.router.yaml") + .replace("", &mock_server.uri()); + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Otlp { + endpoint: Some(format!("{}/v1/traces", mock_server.uri())), + }) + .config(&config) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); + let (id, _) = router.execute_query(&query).await; + Spec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .build() + .validate_trace(id, &mock_server) + .await?; + router.graceful_shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_otlp_request_with_datadog_propagator_no_agent() -> Result<(), BoxError> { + if !graph_os_enabled() { + panic!("Error: test skipped because GraphOS is not enabled"); + } + let mock_server = mock_otlp_server().await; + let config = include_str!("fixtures/otlp_datadog_propagation_no_agent.router.yaml") + .replace("", &mock_server.uri()); + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Otlp { + endpoint: Some(format!("{}/v1/traces", mock_server.uri())), + }) + .config(&config) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); + let (id, _) = router.execute_query(&query).await; + Spec::builder() + .services(["client", "router", "subgraph"].into()) + .build() + .validate_trace(id, &mock_server) + .await?; + router.graceful_shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( ) -> Result<(), BoxError> { - for _ in 0..10 { - let trace_valid = find_valid_trace( - mock_server, - query, - operation_name, - services, - custom_span_instrumentation, - ) + if !graph_os_enabled() { + panic!("Error: test skipped because GraphOS is not enabled"); + } + let mock_server = mock_otlp_server().await; + let config = include_str!("fixtures/otlp_datadog_request_with_zipkin_propagator.router.yaml") + .replace("", &mock_server.uri()); + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Otlp { + endpoint: Some(format!("{}/v1/traces", mock_server.uri())), + }) + .config(&config) + .build() .await; - let metrics_valid = find_valid_metrics(mock_server, query, operation_name, services).await; + router.start().await; + router.assert_started().await; - if metrics_valid.is_ok() && trace_valid.is_ok() { - return Ok(()); - } + let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); + let (id, _) = router.execute_query(&query).await; + + Spec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .build() + .validate_trace(id, &mock_server) + .await?; + // ---------------------- zipkin propagator with unsampled trace + // Testing for an unsampled trace, so it should be sent to the otlp exporter with sampling priority set 0 + // But it shouldn't send the trace to subgraph as the trace is originally not sampled, the main goal is to measure it at the DD agent level + let id = TraceId::from_hex("80f198ee56343ba864fe8b2a57d3eff7").unwrap(); + let headers: HashMap = [ + ( + "X-B3-TraceId".to_string(), + "80f198ee56343ba864fe8b2a57d3eff7".to_string(), + ), + ( + "X-B3-ParentSpanId".to_string(), + "05e3ac9a4f6e3b90".to_string(), + ), + ("X-B3-SpanId".to_string(), "e457b5a2e4d86bd1".to_string()), + ("X-B3-Sampled".to_string(), "0".to_string()), + ] + .into(); + + let (_id, _) = router.execute_untraced_query(&query, Some(headers)).await; + Spec::builder() + .services(["router"].into()) + .priority_sampled("0") + .build() + .validate_trace(id, &mock_server) + .await?; + // ---------------------- trace context propagation + // Testing for a trace containing the right tracestate with m and psr for DD and a sampled trace, so it should be sent to the otlp exporter with sampling priority set to 1 + // And it should also send the trace to subgraph as the trace is sampled + let id = TraceId::from_hex("0af7651916cd43dd8448eb211c80319c").unwrap(); + let headers: HashMap = [ + ( + "traceparent".to_string(), + "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(), + ), + ("tracestate".to_string(), "m=1,psr=1".to_string()), + ] + .into(); + + let (_id, _) = router.execute_untraced_query(&query, Some(headers)).await; + Spec::builder() + .services(["router", "subgraph"].into()) + .priority_sampled("1") + .build() + .validate_trace(id, &mock_server) + .await?; + // ---------------------- + // Testing for a trace containing the right tracestate with m and psr for DD and an unsampled trace, so it should be sent to the otlp exporter with sampling priority set to 0 + // But it shouldn't send the trace to subgraph as the trace is originally not sampled, the main goal is to measure it at the DD agent level + let id = TraceId::from_hex("0af7651916cd43dd8448eb211c80319d").unwrap(); + let headers: HashMap = [ + ( + "traceparent".to_string(), + "00-0af7651916cd43dd8448eb211c80319d-b7ad6b7169203331-00".to_string(), + ), + ("tracestate".to_string(), "m=1,psr=0".to_string()), + ] + .into(); + + let (_id, _) = router.execute_untraced_query(&query, Some(headers)).await; + Spec::builder() + .services(["router"].into()) + .priority_sampled("0") + .build() + .validate_trace(id, &mock_server) + .await?; + // ---------------------- + // Testing for a trace containing a tracestate m and psr with psr set to 1 for DD and an unsampled trace, so it should be sent to the otlp exporter with sampling priority set to 1 + // It should not send the trace to the subgraph as we didn't use the datadog propagator and therefore the trace will remain unsampled. + let id = TraceId::from_hex("0af7651916cd43dd8448eb211c80319e").unwrap(); + let headers: HashMap = [ + ( + "traceparent".to_string(), + "00-0af7651916cd43dd8448eb211c80319e-b7ad6b7169203331-00".to_string(), + ), + ("tracestate".to_string(), "m=1,psr=1".to_string()), + ] + .into(); + + let (_id, _) = router.execute_untraced_query(&query, Some(headers)).await; + Spec::builder() + .services(["router"].into()) + .priority_sampled("1") + .build() + .validate_trace(id, &mock_server) + .await?; + + // Be careful if you add the same kind of test crafting your own trace id, make sure to increment the previous trace id by 1 if not you'll receive all the previous spans tested with the same trace id before + router.graceful_shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_untraced_request_no_sample_datadog_agent() -> Result<(), BoxError> { + if !graph_os_enabled() { + panic!("Error: test skipped because GraphOS is not enabled"); + } + let mock_server = mock_otlp_server().await; + let config = include_str!("fixtures/otlp_datadog_agent_no_sample.router.yaml") + .replace("", &mock_server.uri()); + let mut router = IntegrationTest::builder().config(&config).build().await; + + router.start().await; + router.assert_started().await; + + let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); + let (id, _) = router.execute_untraced_query(&query, None).await; + Spec::builder() + .services(["router"].into()) + .priority_sampled("0") + .build() + .validate_trace(id, &mock_server) + .await?; + router.graceful_shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_untraced_request_sample_datadog_agent() -> Result<(), BoxError> { + if !graph_os_enabled() { + panic!("Error: test skipped because GraphOS is not enabled"); + } + let mock_server = mock_otlp_server().await; + let config = include_str!("fixtures/otlp_datadog_agent_sample.router.yaml") + .replace("", &mock_server.uri()); + let mut router = IntegrationTest::builder().config(&config).build().await; + + router.start().await; + router.assert_started().await; + + let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); + let (id, _) = router.execute_untraced_query(&query, None).await; + Spec::builder() + .services(["router"].into()) + .priority_sampled("1") + .build() + .validate_trace(id, &mock_server) + .await?; + router.graceful_shutdown().await; + Ok(()) +} - tokio::time::sleep(Duration::from_millis(100)).await; +#[tokio::test(flavor = "multi_thread")] +async fn test_untraced_request_sample_datadog_agent_unsampled() -> Result<(), BoxError> { + if !graph_os_enabled() { + panic!("Error: test skipped because GraphOS is not enabled"); } - find_valid_trace( - mock_server, - query, - operation_name, - services, - custom_span_instrumentation, + let mock_server = mock_otlp_server().await; + let config = include_str!("fixtures/otlp_datadog_agent_sample_no_sample.router.yaml") + .replace("", &mock_server.uri()); + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Otlp { + endpoint: Some(format!("{}/v1/traces", mock_server.uri())), + }) + .config(&config) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); + let (id, _) = router.execute_untraced_query(&query, None).await; + Spec::builder() + .services(["router"].into()) + .priority_sampled("0") + .build() + .validate_trace(id, &mock_server) + .await?; + router.graceful_shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_priority_sampling_propagated() -> Result<(), BoxError> { + if !graph_os_enabled() { + panic!("Error: test skipped because GraphOS is not enabled"); + } + let mock_server = mock_otlp_server().await; + let config = include_str!("fixtures/otlp_datadog_propagation.router.yaml") + .replace("", &mock_server.uri()); + let mut router = IntegrationTest::builder() + // We're using datadog propagation as this is what we are trying to test. + .telemetry(Telemetry::Datadog) + .config(config) + .responder(ResponseTemplate::new(200).set_body_json( + json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), + )) + .build() + .await; + + router.start().await; + router.assert_started().await; + + // Parent based sampling. psr MUST be populated with the value that we pass in. + test_psr( + &mut router, + Some("-1"), + Spec::builder() + .services(["router"].into()) + .priority_sampled("-1") + .build(), + &mock_server, + ) + .await?; + test_psr( + &mut router, + Some("0"), + Spec::builder() + .services(["router"].into()) + .priority_sampled("0") + .build(), + &mock_server, + ) + .await?; + test_psr( + &mut router, + Some("1"), + Spec::builder() + .services(["router"].into()) + .priority_sampled("1") + .build(), + &mock_server, + ) + .await?; + test_psr( + &mut router, + Some("2"), + Spec::builder() + .services(["router"].into()) + .priority_sampled("2") + .build(), + &mock_server, + ) + .await?; + + // No psr was passed in the router is free to set it. This will be 1 as we are going to sample here. + test_psr( + &mut router, + None, + Spec::builder() + .services(["router"].into()) + .priority_sampled("1") + .build(), + &mock_server, + ) + .await?; + + router.graceful_shutdown().await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + let mock_server = mock_otlp_server().await; + let config = include_str!("fixtures/otlp_datadog_propagation_no_parent_sampler.router.yaml") + .replace("", &mock_server.uri()); + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Datadog) + .config(config) + .responder(ResponseTemplate::new(200).set_body_json( + json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), + )) + .build() + .await; + + router.start().await; + router.assert_started().await; + + // The router will ignore the upstream PSR as parent based sampling is disabled. + test_psr( + &mut router, + Some("-1"), + Spec::builder() + .services(["router"].into()) + .priority_sampled("1") + .build(), + &mock_server, + ) + .await?; + test_psr( + &mut router, + Some("0"), + Spec::builder() + .services(["router"].into()) + .priority_sampled("1") + .build(), + &mock_server, + ) + .await?; + test_psr( + &mut router, + Some("1"), + Spec::builder() + .services(["router"].into()) + .priority_sampled("1") + .build(), + &mock_server, + ) + .await?; + test_psr( + &mut router, + Some("2"), + Spec::builder() + .services(["router"].into()) + .priority_sampled("1") + .build(), + &mock_server, ) .await?; - find_valid_metrics(mock_server, query, operation_name, services).await?; + + test_psr( + &mut router, + None, + Spec::builder() + .services(["router"].into()) + .priority_sampled("1") + .build(), + &mock_server, + ) + .await?; + + router.graceful_shutdown().await; Ok(()) } -async fn find_valid_trace( +async fn test_psr( + router: &mut IntegrationTest, + psr: Option<&str>, + trace_spec: Spec, mock_server: &MockServer, - _query: &Value, - _operation_name: Option<&str>, - services: &[&'static str], - _custom_span_instrumentation: bool, ) -> Result<(), BoxError> { - let requests = mock_server - .received_requests() - .await - .expect("Could not get otlp requests"); - - // A valid trace has: - // * A valid service name - // * All three services - // * The correct spans - // * All spans are parented - // * Required attributes of 'router' span has been set - let traces: Vec<_>= requests - .iter() - .filter_map(|r| { - if r.url.path().ends_with("/traces") { + let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); + let headers = if let Some(psr) = psr { + vec![("x-datadog-sampling-priority".to_string(), psr.to_string())] + } else { + vec![] + }; + let (id, result) = router + .execute_query_with_headers(&query, headers.into_iter().collect()) + .await; + + assert!(result.status().is_success()); + trace_spec.validate_trace(id, mock_server).await?; + Ok(()) +} + +#[derive(buildstructor::Builder)] +struct Spec { + operation_name: Option, + version: Option, + services: HashSet<&'static str>, + span_names: HashSet<&'static str>, + measured_spans: HashSet<&'static str>, + unmeasured_spans: HashSet<&'static str>, + priority_sampled: Option<&'static str>, +} + +impl Spec { + #[allow(clippy::too_many_arguments)] + async fn validate_trace(&self, id: TraceId, mock_server: &MockServer) -> Result<(), BoxError> { + for _ in 0..10 { + if self.find_valid_trace(id, mock_server).await.is_ok() { + return Ok(()); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + self.find_valid_trace(id, mock_server).await?; + Ok(()) + } + + async fn validate_metrics(&self, mock_server: &MockServer) -> Result<(), BoxError> { + for _ in 0..10 { + if self.find_valid_metrics(mock_server).await.is_ok() { + return Ok(()); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + self.find_valid_metrics(mock_server).await?; + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + async fn find_valid_trace( + &self, + trace_id: TraceId, + mock_server: &MockServer, + ) -> Result<(), BoxError> { + // A valid trace has: + // * All three services + // * The correct spans + // * All spans are parented + // * Required attributes of 'router' span has been set + + let requests = mock_server.received_requests().await; + let trace= Value::Array(requests.unwrap_or_default().iter().filter(|r| r.url.path().ends_with("/traces")) + .filter_map(|r|{ match opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest::decode( bytes::Bytes::copy_from_slice(&r.body), ) { Ok(trace) => { match serde_json::to_value(trace) { - Ok(trace) => { Some(Ok(trace)) } - Err(e) => { - Some(Err(BoxError::from(format!("failed to decode trace: {}", e)))) + Ok(trace) => { + Some(trace) } + Err(_) => { + None } } } - Err(e) => { - Some(Err(BoxError::from(format!("failed to decode trace: {}", e)))) + Err(_) => { + None } } + }).filter(|t| { + + let datadog_trace_id = TraceId::from_u128(trace_id.to_datadog() as u128); + let trace_found1 = !t.select_path(&format!("$..[?(@.traceId == '{}')]", trace_id)).unwrap_or_default().is_empty(); + let trace_found2 = !t.select_path(&format!("$..[?(@.traceId == '{}')]", datadog_trace_id)).unwrap_or_default().is_empty(); + trace_found1 | trace_found2 + }).collect()); + + self.verify_services(&trace)?; + self.verify_spans_present(&trace)?; + self.verify_measured_spans(&trace)?; + self.verify_operation_name(&trace)?; + self.verify_priority_sampled(&trace)?; + self.verify_version(&trace)?; + self.verify_span_kinds(&trace)?; + + Ok(()) + } + + fn verify_version(&self, trace: &Value) -> Result<(), BoxError> { + if let Some(expected_version) = &self.version { + let binding = trace.select_path("$..version")?; + let version = binding.first(); + assert_eq!( + version + .expect("version expected") + .as_str() + .expect("version must be a string"), + expected_version + ); + } + Ok(()) + } + + fn verify_measured_spans(&self, trace: &Value) -> Result<(), BoxError> { + for expected in &self.measured_spans { + assert!( + self.measured_span(trace, expected)?, + "missing measured span {}", + expected + ); + } + for unexpected in &self.unmeasured_spans { + assert!( + !self.measured_span(trace, unexpected)?, + "unexpected measured span {}", + unexpected + ); + } + Ok(()) + } + + fn measured_span(&self, trace: &Value, name: &str) -> Result { + let binding1 = trace.select_path(&format!( + "$..[?(@.meta.['otel.original_name'] == '{}')].metrics.['_dd.measured']", + name + ))?; + let binding2 = trace.select_path(&format!( + "$..[?(@.name == '{}')].metrics.['_dd.measured']", + name + ))?; + Ok(binding1 + .first() + .or(binding2.first()) + .and_then(|v| v.as_f64()) + .map(|v| v == 1.0) + .unwrap_or_default()) + } + + fn verify_span_kinds(&self, trace: &Value) -> Result<(), BoxError> { + // Validate that the span.kind has been propagated. We can just do this for a selection of spans. + self.validate_span_kind(trace, "router", "server")?; + self.validate_span_kind(trace, "supergraph", "internal")?; + self.validate_span_kind(trace, "http_request", "client")?; + Ok(()) + } + + fn verify_services(&self, trace: &Value) -> Result<(), BoxError> { + let actual_services: HashSet = trace + .select_path("$..resource.attributes..[?(@.key == 'service.name')].value.stringValue")? + .into_iter() + .filter_map(|service| service.as_string()) + .collect(); + tracing::debug!("found services {:?}", actual_services); + let expected_services = self + .services + .iter() + .map(|s| s.to_string()) + .collect::>(); + if actual_services != expected_services { + return Err(BoxError::from(format!( + "incomplete traces, got {actual_services:?} expected {expected_services:?}" + ))); + } + Ok(()) + } + + fn verify_spans_present(&self, trace: &Value) -> Result<(), BoxError> { + let operation_names: HashSet = trace + .select_path("$..spans..name")? + .into_iter() + .filter_map(|span_name| span_name.as_string()) + .collect(); + let mut span_names: HashSet<&str> = self.span_names.clone(); + if self.services.contains("client") { + span_names.insert("client_request"); + } + tracing::debug!("found spans {:?}", operation_names); + let missing_operation_names: Vec<_> = span_names + .iter() + .filter(|o| !operation_names.contains(**o)) + .collect(); + if !missing_operation_names.is_empty() { + return Err(BoxError::from(format!( + "spans did not match, got {operation_names:?}, missing {missing_operation_names:?}" + ))); + } + Ok(()) + } + + fn validate_span_kind(&self, trace: &Value, name: &str, kind: &str) -> Result<(), BoxError> { + let kind = match kind { + "internal" => 1, + "client" => 3, + "server" => 2, + _ => panic!("unknown kind"), + }; + let binding1 = trace.select_path(&format!( + "$..spans..[?(@.kind == {})]..[?(@.key == 'otel.original_name')].value..[?(@ == '{}')]", + kind, name + ))?; + let binding2 = trace.select_path(&format!( + "$..spans..[?(@.kind == {} && @.name == '{}')]", + kind, name + ))?; + let binding = binding1.first().or(binding2.first()); + + if binding.is_none() { + return Err(BoxError::from(format!( + "span.kind missing or incorrect {}, {}", + name, kind + ))); + } + Ok(()) + } + + fn verify_operation_name(&self, trace: &Value) -> Result<(), BoxError> { + if let Some(expected_operation_name) = &self.operation_name { + let binding = + trace.select_path("$..[?(@.name == 'supergraph')]..[?(@.key == 'graphql.operation.name')].value.stringValue")?; + let operation_name = binding.first(); + assert_eq!( + operation_name + .expect("graphql.operation.name expected") + .as_str() + .expect("graphql.operation.name must be a string"), + expected_operation_name + ); + } + Ok(()) + } + + fn verify_priority_sampled(&self, trace: &Value) -> Result<(), BoxError> { + if let Some(psr) = self.priority_sampled { + let binding = trace.select_path( + "$..[?(@.name == 'execution')]..[?(@.key == 'sampling.priority')].value.intValue", + )?; + if binding.is_empty() { + return Err(BoxError::from("missing sampling priority")); } - else { - None + for sampling_priority in binding { + assert_eq!( + sampling_priority + .as_i64() + .expect("psr not an integer") + .to_string(), + psr + ); } - }) - .try_collect()?; - if !traces.is_empty() { - let json_trace = serde_json::Value::Array(traces); - verify_trace_participants(&json_trace, services)?; - + } else { + assert!(trace.select_path("$..[?(@.name == 'execution')]..[?(@.key == 'sampling.priority')].value.intValue")?.is_empty()) + } Ok(()) - } else { - Err(anyhow!("No traces received").into()) } -} -fn verify_trace_participants(trace: &Value, services: &[&'static str]) -> Result<(), BoxError> { - let actual_services: HashSet = trace - .select_path("$..resource.attributes[?(@.key=='service.name')].value.stringValue")? - .into_iter() - .filter_map(|service| service.as_string()) - .collect(); - tracing::debug!("found services {:?}", actual_services); - - let expected_services = services - .iter() - .map(|s| s.to_string()) - .collect::>(); - if actual_services != expected_services { - return Err(BoxError::from(format!( - "incomplete traces, got {actual_services:?} expected {expected_services:?}" - ))); + async fn find_valid_metrics(&self, mock_server: &MockServer) -> Result<(), BoxError> { + let requests = mock_server + .received_requests() + .await + .expect("Could not get otlp requests"); + if let Some(metrics) = requests.iter().find(|r| r.url.path().ends_with("/metrics")) { + let metrics = opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest::decode(bytes::Bytes::copy_from_slice(&metrics.body))?; + let json_metrics = serde_json::to_value(metrics)?; + // For now just validate service name. + self.verify_services(&json_metrics)?; + + Ok(()) + } else { + Err(anyhow!("No metrics received").into()) + } } - Ok(()) } -fn validate_service_name(trace: Value) -> Result<(), BoxError> { - let service_name = - trace.select_path("$..resource.attributes[?(@.key=='service.name')].value.stringValue")?; - assert_eq!( - service_name.first(), - Some(&&Value::String("router".to_string())) - ); - Ok(()) +async fn mock_otlp_server() -> MockServer { + let mock_server = wiremock::MockServer::start().await; + Mock::given(method("POST")) + .and(path("/v1/traces")) + .respond_with(ResponseTemplate::new(200).set_body_raw( + ExportTraceServiceResponse::default().encode_to_vec(), + "application/x-protobuf", + )) + .expect(1..) + .mount(&mock_server) + .await; + Mock::given(method("POST")) + .and(path("/metrics")) + .respond_with(ResponseTemplate::new(200).set_body_raw( + ExportMetricsServiceResponse::default().encode_to_vec(), + "application/x-protobuf", + )) + .expect(1..) + .mount(&mock_server) + .await; + mock_server } -async fn find_valid_metrics( - mock_server: &MockServer, - _query: &Value, - _operation_name: Option<&str>, - _services: &[&'static str], -) -> Result<(), BoxError> { - let requests = mock_server - .received_requests() - .await - .expect("Could not get otlp requests"); - if let Some(metrics) = requests.iter().find(|r| r.url.path().ends_with("/metrics")) { - let metrics = opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest::decode(bytes::Bytes::copy_from_slice(&metrics.body))?; - let json_trace = serde_json::to_value(metrics)?; - // For now just validate service name. - validate_service_name(json_trace)?; - - Ok(()) - } else { - Err(anyhow!("No metrics received").into()) +pub(crate) trait DatadogId { + fn to_datadog(&self) -> u64; +} +impl DatadogId for TraceId { + fn to_datadog(&self) -> u64 { + let bytes = &self.to_bytes()[std::mem::size_of::()..std::mem::size_of::()]; + u64::from_be_bytes(bytes.try_into().unwrap()) } } diff --git a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx index 0eea7691d9..e91b12508e 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx @@ -12,11 +12,16 @@ For general tracing configuration, refer to [Router Tracing Configuration](./ove ## OTLP configuration -To export traces to Datadog via OTLP, you must do the following: -- Configure the Datadog agent to accept OTLP traces. -- Configure the router to send traces to the Datadog agent. +OTLP is the [OpenTelemetry protocol](https://opentelemetry.io/docs/specs/otel/protocol/), and is the recommended protocol for transmitting telemetry, including traces, to Datadog. -To configure the Datadog agent, add OTLP configuration to your `datadog.yaml`. For example: +To setup traces to Datadog via OTLP, you must do the following: + +- Modify the default configuration of the Datadog Agent to accept OTLP traces submitted to it by the router. +- Configure the router to send traces to the configured Datadog Agent. + +### Datadog Agent configuration + +To configure the Datadog Agent, add OTLP configuration to your `datadog.yaml`. For example: ```yaml title="datadog.yaml" otlp_config: @@ -26,26 +31,42 @@ otlp_config: endpoint: :4317 ``` -To configure the router, enable the [OTLP exporter](./otlp) and set `endpoint: `. For example: +For additional Datadog Agent configuration details, review Datadog's [Enabling OTLP Ingestion on the Datadog Agent](https://docs.datadoghq.com/opentelemetry/interoperability/otlp_ingest_in_the_agent/?tab=host#enabling-otlp-ingestion-on-the-datadog-agent) documentation. + +### Router configuration + +To configure the router, enable the [OTLP exporter](./otlp) and set `endpoint: `. For example: ```yaml title="router.yaml" telemetry: exporters: tracing: + common: + # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + sampler: 0.1 + otlp: enabled: true - # Optional endpoint, either 'default' or a URL (Defaults to http://127.0.0.1:4317) endpoint: "${env.DATADOG_AGENT_HOST}:4317" + # Optional batch processor setting, this will enable the batch processor to send concurrent requests in a high load scenario. + batch_processor: + max_concurrent_exports: 100 ``` -For more details about Datadog configuration, see [Datadog Agent configuration](https://docs.datadoghq.com/opentelemetry/otlp_ingest_in_the_agent/?tab=host). +Adjusting the `sampler` will allow you to control the sampling decisions that the router will make on its own and decrease the rate at which you sample, which can have a direct impact on your your Datadog bill. + + + +Depending on the volume of spans being created in a router instance, it will be necessary to adjust the `batch_processor` settings in your `exporter` config. If this is necessary, you will see warning messages from the router regarding the batch span processor. This applies to both OTLP and the Datadog native exporter. + + ### Enabling log correlation To enable Datadog log correlation, you must configure `dd.trace_id` to appear on the `router` span: - + ```yaml title="router.yaml" telemetry: instrumentation: @@ -72,10 +93,18 @@ The router can be configured to connect to either the native, default Datadog ag telemetry: exporters: tracing: - datadog: - enabled: true - # Optional endpoint, either 'default' or a URL (Defaults to http://127.0.0.1:8126) - endpoint: "http://${env.DATADOG_AGENT_HOST}:8126" + common: + # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + sampler: 0.1 + + datadog: + enabled: true + # Optional endpoint, either 'default' or a URL (Defaults to http://127.0.0.1:8126) + endpoint: "http://${env.DATADOG_AGENT_HOST}:8126" + + # Optional batch processor setting, this will enable the batch processor to send concurrent requests in a high load scenario. + batch_processor: + max_concurrent_exports: 100 # Enable graphql.operation.name attribute on supergraph spans. instrumentation: @@ -86,6 +115,12 @@ telemetry: graphql.operation.name: true ``` + + +Depending on the volume of spans being created in a router instance, it will be necessary to adjust the `batch_processor` settings in your `exporter` config. This applies to both OTLP and the Datadog native exporter. + + + ### `enabled` Set to true to enable the Datadog exporter. Defaults to false. @@ -227,11 +262,11 @@ If you have introduced a new span in a custom build of the Router you can enable telemetry: exporters: tracing: - datadog: - batch_processor: + datadog: + batch_processor: max_export_batch_size: 512 max_concurrent_exports: 1 - max_export_timeout: 30s + max_export_timeout: 30s max_queue_size: 2048 scheduled_delay: 5s ``` From 42247b7b173829119cad724a9f069cf6d7053d72 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 4 Oct 2024 09:23:27 +0100 Subject: [PATCH 02/35] Update docs --- .../telemetry/exporters/tracing/datadog.mdx | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx index e91b12508e..1ded523ec2 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx @@ -43,6 +43,7 @@ telemetry: tracing: common: # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + preview_datadog_agent_sampling: true sampler: 0.1 otlp: @@ -55,7 +56,7 @@ telemetry: max_concurrent_exports: 100 ``` -Adjusting the `sampler` will allow you to control the sampling decisions that the router will make on its own and decrease the rate at which you sample, which can have a direct impact on your your Datadog bill. +Adjusting the `sampler` will allow you to control the sampling decisions that the router will make on its own and decrease the rate at which you sample, which can have a direct impact on your Datadog bill. @@ -63,6 +64,28 @@ Depending on the volume of spans being created in a router instance, it will be +### Enabling Datadog Agent sampling + +The Datadog APM view relies on traces to generate metrics. For this to be accurate 100% of requests must be sampled and sent to the Datadog agent. +To prevent ALL traces from then being sent to Datadog, you must set `preview_datadog_agent_sampling` to `true` and adjust the `sampler` to the desired percentage of traces to be sent to Datadog. + +```yaml title="router.yaml" +telemetry: + exporters: + tracing: + common: + # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + preview_datadog_agent_sampling: true + sampler: 0.1 +``` + + + + Using `preview_datadog_agent_sampling` will send ALL spans to the Datadog agent, but only the `sampler` percentage of them will be forwarded to Datadog. This means that your APM view will be correct at the cost of + the router sending more spans to the Datadog agent. This will have an impact on the resource usage and performance of both the Router and Datadog agent. + + + ### Enabling log correlation To enable Datadog log correlation, you must configure `dd.trace_id` to appear on the `router` span: @@ -95,6 +118,7 @@ telemetry: tracing: common: # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + preview_datadog_agent_sampling: true sampler: 0.1 datadog: @@ -286,3 +310,24 @@ telemetry: | `resource_mapping` | See [config](#resource_mapping) | A map of span names to attribute names. | | `span_metrics` | See [config](#span_metrics) | A map of span names to boolean. | +## `preview_datadog_agent_sampling` (default: `false`) + +The Datadog APM view relies on traces to generate metrics. For this to be accurate 100% of requests must be sampled and sent to the Datadog agent. +To prevent ALL traces from then being sent to Datadog, you must set `preview_datadog_agent_sampling` to `true` and adjust the `sampler` to the desired percentage of traces to be sent to Datadog. + +```yaml title="router.yaml" +telemetry: + exporters: + tracing: + common: + # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + preview_datadog_agent_sampling: true + sampler: 0.1 +``` + + + + Using `preview_datadog_agent_sampling` will send ALL spans to the Datadog agent, but only the `sampler` percentage of them will be forwarded to Datadog. This means that your APM view will be correct at the cost of + the router sending more spans to the Datadog agent. This will have an impact on the resource usage and performance of both the Router and Datadog agent. + + \ No newline at end of file From 7ad4d02e22586cf37c89fa2909e163b9046e52d0 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 4 Oct 2024 10:20:45 +0100 Subject: [PATCH 03/35] Update docs --- .../telemetry/exporters/tracing/datadog.mdx | 82 ++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx index 1ded523ec2..3c22af6b88 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx @@ -310,7 +310,83 @@ telemetry: | `resource_mapping` | See [config](#resource_mapping) | A map of span names to attribute names. | | `span_metrics` | See [config](#span_metrics) | A map of span names to boolean. | -## `preview_datadog_agent_sampling` (default: `false`) +## Sampler configuration + +When using Datadog you will need to take into consideration if you want to use the Datadog APM view or rely on OTLP metrics to gain insight into the Router's performance. +The Datadog APM vie is driven by traces, and for this to be accurate 100% of requests must be sampled and sent to the Datadog agent. + +Tracing is expensive both in terms of APM costs but also Router performance, and typically you will want to set the `sampler` to a low value ion production environments. +However, this will mean that the APM view will only show a small percentage of traces. + +Datadog agent sampling is a mode where ALL traces are sent to the Datadog agent, but only a percentage of them are forwarded to Datadog. This makes the APM view accurate while keeping costs low +at the cost of the Router having an effective sample rate of 100% under the hood. + +Here are some guides on how to configure the `sampler` and `preview_datadog_agent_sampling` to get the desired behavior: + +**I want the APM view to show metrics for 100% of traffic, and I am OK with the performance impact on the Router.** + +Set `preview_datadog_agent_sampling` to `true` and adjust the `sampler` to the desired percentage of traces to be sent to Datadog. + +```yaml title="router.yaml" +telemetry: + exporters: + tracing: + common: + # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + preview_datadog_agent_sampling: true + sampler: 0.1 +``` + +**I want the Datadog agent to be in control of the percentage of traces sent to Datadog.** + +Use the Datadog agent `probabalistic_sampling` option sampler and set the `sampler` to `1` to allow the Datadog agent to control the sampling rate. + +Router config: +```yaml title="router.yaml" +telemetry: + exporters: + tracing: + common: + sampler: always_on +``` + +Datadog agent config: +```yaml +otlp_config: + traces: + probabilistic_sampling: + sampling_percentage: 10 +``` + +**I want the most performance from the Router and am not concerned with the APM view. I use metrics and traces to monitor my application.** + +Set the `sample` to a low value to reduce the number of traces sent to Datadog. Leave `preview_datadog_agent_sampling` to `false`. + +```yaml title="router.yaml" +telemetry: + exporters: + tracing: + common: + sampler: 0.1 + preview_datadog_agent_sampling: false +``` + +### `sampler` (default: `always_on`) + +The `sampler` configuration allows you to control the sampling decisions that the router will make on its own and decrease the rate at which you sample, which can have a direct impact on your Datadog bill. + +```yaml title="router.yaml" +telemetry: + exporters: + tracing: + common: + # Only 10 percent of spans will be forwarded to the Datadog agent. Experiment to find a value that is good for you! + sampler: 0.1 +``` + +If you are using the Datadog APM viw then you should set `preview_datadog_agent_sampling` to `true` and adjust the `sampler` to the desired percentage of traces to be sent to Datadog. + +### `preview_datadog_agent_sampling` (default: `false`) The Datadog APM view relies on traces to generate metrics. For this to be accurate 100% of requests must be sampled and sent to the Datadog agent. To prevent ALL traces from then being sent to Datadog, you must set `preview_datadog_agent_sampling` to `true` and adjust the `sampler` to the desired percentage of traces to be sent to Datadog. @@ -328,6 +404,8 @@ telemetry: Using `preview_datadog_agent_sampling` will send ALL spans to the Datadog agent, but only the `sampler` percentage of them will be forwarded to Datadog. This means that your APM view will be correct at the cost of - the router sending more spans to the Datadog agent. This will have an impact on the resource usage and performance of both the Router and Datadog agent. + the Router sending more spans to the Datadog agent. This will have an impact on the resource usage and performance of both the Router and Datadog agent. + + If you are OK with your APM view only showing a subset of traces, then you can leave `preview_datadog_agent_sampling` to `false`, however it is recommended to rely on OTLP metrics to gain insight into the Router's performance. \ No newline at end of file From d8bf3528c00d56819b9ad4fadbdba5686564f9c2 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 4 Oct 2024 10:47:47 +0100 Subject: [PATCH 04/35] Update docs --- .../telemetry/exporters/tracing/datadog.mdx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx index 3c22af6b88..4de2c86759 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx @@ -313,12 +313,12 @@ telemetry: ## Sampler configuration When using Datadog you will need to take into consideration if you want to use the Datadog APM view or rely on OTLP metrics to gain insight into the Router's performance. -The Datadog APM vie is driven by traces, and for this to be accurate 100% of requests must be sampled and sent to the Datadog agent. +The Datadog APM view is driven by traces, and for this to be accurate 100% of requests must be sampled and sent to the Datadog agent. Tracing is expensive both in terms of APM costs but also Router performance, and typically you will want to set the `sampler` to a low value ion production environments. However, this will mean that the APM view will only show a small percentage of traces. -Datadog agent sampling is a mode where ALL traces are sent to the Datadog agent, but only a percentage of them are forwarded to Datadog. This makes the APM view accurate while keeping costs low +Datadog Agent sampling is a mode where ALL traces are sent to the Datadog agent, but only a percentage of them are forwarded to Datadog. This makes the APM view accurate while keeping costs low at the cost of the Router having an effective sample rate of 100% under the hood. Here are some guides on how to configure the `sampler` and `preview_datadog_agent_sampling` to get the desired behavior: @@ -332,7 +332,8 @@ telemetry: exporters: tracing: common: - # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + # All requests will be traced and sent to the Datadog agent. + # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. preview_datadog_agent_sampling: true sampler: 0.1 ``` @@ -347,6 +348,7 @@ telemetry: exporters: tracing: common: + # All requests will be traced and sent to the Datadog agent. sampler: always_on ``` @@ -355,6 +357,7 @@ Datadog agent config: otlp_config: traces: probabilistic_sampling: + # Only 10 percent of spans will be forwarded to Datadog sampling_percentage: 10 ``` @@ -367,6 +370,7 @@ telemetry: exporters: tracing: common: + # Only 10 percent of requests will be traced and sent to the Datadog agent. The APM view will only show a subset of total request data but the Router will perform better. sampler: 0.1 preview_datadog_agent_sampling: false ``` From 110917cac72c32571031e1914e1d589700bf51b8 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 4 Oct 2024 10:55:31 +0100 Subject: [PATCH 05/35] Update docs --- .../configuration/telemetry/exporters/tracing/datadog.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx index 4de2c86759..c98d570142 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx @@ -321,7 +321,7 @@ However, this will mean that the APM view will only show a small percentage of t Datadog Agent sampling is a mode where ALL traces are sent to the Datadog agent, but only a percentage of them are forwarded to Datadog. This makes the APM view accurate while keeping costs low at the cost of the Router having an effective sample rate of 100% under the hood. -Here are some guides on how to configure the `sampler` and `preview_datadog_agent_sampling` to get the desired behavior: +Use the following guidelines on how to configure the `sampler` and `preview_datadog_agent_sampling` to get the desired behavior: **I want the APM view to show metrics for 100% of traffic, and I am OK with the performance impact on the Router.** @@ -340,7 +340,7 @@ telemetry: **I want the Datadog agent to be in control of the percentage of traces sent to Datadog.** -Use the Datadog agent `probabalistic_sampling` option sampler and set the `sampler` to `1` to allow the Datadog agent to control the sampling rate. +Use the Datadog agent `probabalistic_sampling` option sampler and set the `sampler` to `always_on` to allow the Datadog agent to control the sampling rate. Router config: ```yaml title="router.yaml" From 562b7ac0456a011a348d32d0cd9fe4114b30d1c8 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 4 Oct 2024 11:12:22 +0100 Subject: [PATCH 06/35] Split the changelogs --- ....md => fix_bryn_datadog_agent_sampling.md} | 19 ++++--------------- ..._upstream_sampling_decision_propagation.md | 7 +++++++ 2 files changed, 11 insertions(+), 15 deletions(-) rename .changesets/{fix_bryn_datadog_upstream_sampling_decision_test.md => fix_bryn_datadog_agent_sampling.md} (57%) create mode 100644 .changesets/fix_bryn_datadog_upstream_sampling_decision_propagation.md diff --git a/.changesets/fix_bryn_datadog_upstream_sampling_decision_test.md b/.changesets/fix_bryn_datadog_agent_sampling.md similarity index 57% rename from .changesets/fix_bryn_datadog_upstream_sampling_decision_test.md rename to .changesets/fix_bryn_datadog_agent_sampling.md index bb6447a66b..1bea4a5277 100644 --- a/.changesets/fix_bryn_datadog_upstream_sampling_decision_test.md +++ b/.changesets/fix_bryn_datadog_agent_sampling.md @@ -1,11 +1,6 @@ -### Respect x-datadog-sampling-priority ([PR #6017](https://github.com/apollographql/router/pull/6017)) +### Add `preview_datadog_agent_sampling` ([PR #6017](https://github.com/apollographql/router/pull/6017)) -This PR consists of two fixes: -#### Datadog priority sampling resolution is not lost. - -Previously a `x-datadog-sampling-priority` of `-1` would be converted to `0` for downstream requests and `2` would be converted to `1`. - -#### The sampler option in the `telemetry.exporters.tracing.common.sampler` is not datadog aware. +The sampler option in the `telemetry.exporters.tracing.common.sampler` is not datadog aware. To get accurate APM metrics all spans must be sent to the datadog agent with a `psr` or `sampling.priority` attribute set appropriately to record the sampling decision. @@ -24,25 +19,19 @@ telemetry: # Example OTLP exporter configuration otlp: enabled: true - # Optional batch processor setting, this will enable the batch processor to send concurrent requests in a high load scenario. - batch_processor: - max_concurrent_exports: 100 # Example Datadog native exporter configuration datadog: enabled: true - # Optional batch processor setting, this will enable the batch processor to send concurrent requests in a high load scenario. - batch_processor: - max_concurrent_exports: 100 ``` By using these options, you can decrease your Datadog bill as you will only be sending a percentage of spans from the Datadog agent to datadog. > [!IMPORTANT] -> Users must enable `preview_datadog_agent_sampling` to get accurate APM metrics. +> Users must enable `preview_datadog_agent_sampling` to get accurate APM metrics. Users that have been using recent versions of the router will have to modify their configuration to retain full APM metrics. > [!IMPORTANT] > Sending all spans to the datadog agent may require that you tweak the `batch_processor` settings in your exporter config. This applies to both OTLP and the Datadog native exporter. -By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/6017 +See the updated Datadog tracing documentation for more information on configuration options and their implications. \ No newline at end of file diff --git a/.changesets/fix_bryn_datadog_upstream_sampling_decision_propagation.md b/.changesets/fix_bryn_datadog_upstream_sampling_decision_propagation.md new file mode 100644 index 0000000000..c67d135689 --- /dev/null +++ b/.changesets/fix_bryn_datadog_upstream_sampling_decision_propagation.md @@ -0,0 +1,7 @@ +### Datadog priority sampling resolution is lost ([PR #6017](https://github.com/apollographql/router/pull/6017)) + +Previously a `x-datadog-sampling-priority` of `-1` would be converted to `0` for downstream requests and `2` would be converted to `1`. +This means that when propagating to downstream services a value of USER_REJECT would be transmitted as AUTO_REJECT. + +This PR fixes this by ensuring that the `x-datadog-sampling-priority` is transmitted as is to downstream services. + From 1776e5983a93e523cbd9dce31dafaeefe209fb02 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 4 Oct 2024 11:13:01 +0100 Subject: [PATCH 07/35] Remove commented out code for auto enabling sampling --- apollo-router/src/plugins/telemetry/mod.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index fa6fa6494f..dabde7206a 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -286,20 +286,6 @@ impl Plugin for Telemetry { .expect("otel error handler lock poisoned, fatal"); let mut config = init.config; - // This code would have enabled datadog agent sampling by default, but for now we will leave it as opt-in. - // If the datadog exporter is enabled then enable the agent sampler. - // If users are using otlp export then they will need to set this explicitly in their config. - // - // if config.exporters.tracing.datadog.enabled() - // && config - // .exporters - // .tracing - // .common - // .preview_datadog_agent_sampling - // .is_none() - // { - // config.exporters.tracing.common.preview_datadog_agent_sampling = Some(true); - // } config.instrumentation.spans.update_defaults(); config.instrumentation.instruments.update_defaults(); config.exporters.logging.validate()?; From 9b60c38d45a827a0e09f9d3163b9d0e84b734735 Mon Sep 17 00:00:00 2001 From: bryn Date: Wed, 16 Oct 2024 10:07:05 +0100 Subject: [PATCH 08/35] Update docs to indicate that preview datadog agent sampling will NOT use rate based sampling from agent. --- .../fix_bryn_datadog_agent_sampling.md | 13 ++--- .../telemetry/exporters/tracing/datadog.mdx | 11 +++-- .../telemetry/exporters/tracing/overview.mdx | 49 ++++++++++++++----- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/.changesets/fix_bryn_datadog_agent_sampling.md b/.changesets/fix_bryn_datadog_agent_sampling.md index 1bea4a5277..71d6306017 100644 --- a/.changesets/fix_bryn_datadog_agent_sampling.md +++ b/.changesets/fix_bryn_datadog_agent_sampling.md @@ -15,14 +15,7 @@ telemetry: sampler: 0.1 # Send all spans to the Datadog agent. preview_datadog_agent_sampling: true - - # Example OTLP exporter configuration - otlp: - enabled: true - - # Example Datadog native exporter configuration - datadog: - enabled: true + ``` @@ -31,6 +24,10 @@ By using these options, you can decrease your Datadog bill as you will only be s > [!IMPORTANT] > Users must enable `preview_datadog_agent_sampling` to get accurate APM metrics. Users that have been using recent versions of the router will have to modify their configuration to retain full APM metrics. +> [!IMPORTANT] +> The Router does not support [`in-agent` ingestion control](https://docs.datadoghq.com/tracing/trace_pipeline/ingestion_mechanisms/?tab=java#in-the-agent). +> Configuring `traces_per_second` in the Datadog agent will NOT dynamically adjust the Router's sampling rate to meet the target rate. + > [!IMPORTANT] > Sending all spans to the datadog agent may require that you tweak the `batch_processor` settings in your exporter config. This applies to both OTLP and the Datadog native exporter. diff --git a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx index c98d570142..1e1d5d0013 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx @@ -75,15 +75,16 @@ telemetry: tracing: common: # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! - preview_datadog_agent_sampling: true sampler: 0.1 + preview_datadog_agent_sampling: true ``` - - Using `preview_datadog_agent_sampling` will send ALL spans to the Datadog agent, but only the `sampler` percentage of them will be forwarded to Datadog. This means that your APM view will be correct at the cost of - the router sending more spans to the Datadog agent. This will have an impact on the resource usage and performance of both the Router and Datadog agent. - + The Router does not support [`in-agent` ingestion control](https://docs.datadoghq.com/tracing/trace_pipeline/ingestion_mechanisms/?tab=java#in-the-agent). + Configuring `traces_per_second` in the Datadog agent will not dynamically adjust the Router's sampling rate to meet the target rate. + + + Using `preview_datadog_agent_sampling` will send ALL spans to the Datadog agent. This will have an impact on the resource usage and performance of both the Router and Datadog agent. ### Enabling log correlation diff --git a/docs/source/configuration/telemetry/exporters/tracing/overview.mdx b/docs/source/configuration/telemetry/exporters/tracing/overview.mdx index c7b81cca70..d9663a8ec6 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/overview.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/overview.mdx @@ -111,6 +111,29 @@ telemetry: - `parent_based_sampler` enables clients to make the sampling decision. This guarantees that a trace that starts at a client will also have spans at the router. You may wish to disable it (setting `parent_based_sampler: false`) if your router is exposed directly to the internet. +### `preview_datadog_agent_sampling` + +The Datadog APM view relies on traces to generate metrics. For this to be accurate 100% of requests must be sampled and sent to the Datadog agent. +To prevent ALL traces from then being sent to Datadog, you must set `preview_datadog_agent_sampling` to `true` and adjust the `sampler` to the desired percentage of traces to be sent to Datadog. + +```yaml title="router.yaml" +telemetry: + exporters: + tracing: + common: + # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + sampler: 0.1 + preview_datadog_agent_sampling: true +``` + + + The Router does not support [`in-agent` ingestion control](https://docs.datadoghq.com/tracing/trace_pipeline/ingestion_mechanisms/?tab=java#in-the-agent). + Configuring `traces_per_second` in the Datadog agent will not dynamically adjust the Router's sampling rate to meet the target rate. + + + Using `preview_datadog_agent_sampling` will send ALL spans to the Datadog agent. This will have an impact on the resource usage and performance of both the Router and Datadog agent. + + ### `propagation` The `telemetry.exporters.tracing.propagation` section allows you to configure which propagators are active in addition to those automatically activated by using an exporter. @@ -235,17 +258,21 @@ Using this configuration you will have a response header called `my-trace-id` co ## Tracing common reference -| Attribute | Default | Description | -|----------------------------------|--------------------------|-------------------------------------------------| -| `service_name` | `unknown_service:router` | The OpenTelemetry service name. | -| `service_namespace` | | The OpenTelemetry namespace. | -| `resource` | | The OpenTelemetry resource to attach to traces. | -| `experimental_response_trace_id` | | Return the trace ID in a response header. | -| `max_attributes_per_event` | 128 | The maximum number of attributes per event. | -| `max_attributes_per_link` | 128 | The maximum number of attributes per link. | -| `max_attributes_per_span` | 128 | The maximum number of attributes per span. | -| `max_events_per_span` | 128 | The maximum number of events per span. | -| `max_links_per_span` | 128 | The maximum links per span. | +| Attribute | Default | Description | +|----------------------------------|--------------------------|--------------------------------------------------| +| `parent_based_sampler` | `true` | Sampling decisions from upstream will be honored | +| `preview_datadog_agent_sampling` | `false` | Send all spans to the Datadog agent. | +| `propagation` | | The propagation configuration. | +| `sampler` | `always_on` | The sampling rate for traces. | +| `service_name` | `unknown_service:router` | The OpenTelemetry service name. | +| `service_namespace` | | The OpenTelemetry namespace. | +| `resource` | | The OpenTelemetry resource to attach to traces. | +| `experimental_response_trace_id` | | Return the trace ID in a response header. | +| `max_attributes_per_event` | 128 | The maximum number of attributes per event. | +| `max_attributes_per_link` | 128 | The maximum number of attributes per link. | +| `max_attributes_per_span` | 128 | The maximum number of attributes per span. | +| `max_events_per_span` | 128 | The maximum number of events per span. | +| `max_links_per_span` | 128 | The maximum links per span. | ## Related topics From 16cd222a91a8f14e56f73991192e9f0d1726c986 Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Fri, 25 Oct 2024 09:48:34 +0100 Subject: [PATCH 09/35] Update docs/source/configuration/telemetry/exporters/tracing/datadog.mdx Co-authored-by: Edward Huang --- .../configuration/telemetry/exporters/tracing/datadog.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx index 1e1d5d0013..fcd32dc4a0 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx @@ -362,7 +362,7 @@ otlp_config: sampling_percentage: 10 ``` -**I want the most performance from the Router and am not concerned with the APM view. I use metrics and traces to monitor my application.** +**I want the best performance from the router and I'm not concerned with the APM view. I use metrics and traces to monitor my application.** Set the `sample` to a low value to reduce the number of traces sent to Datadog. Leave `preview_datadog_agent_sampling` to `false`. From d7532cedcdccc0057f948910a54aa9b9e323b1ed Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Fri, 25 Oct 2024 09:48:47 +0100 Subject: [PATCH 10/35] Update docs/source/configuration/telemetry/exporters/tracing/datadog.mdx Co-authored-by: Edward Huang --- .../configuration/telemetry/exporters/tracing/datadog.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx index fcd32dc4a0..f81989500c 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx @@ -12,7 +12,7 @@ For general tracing configuration, refer to [Router Tracing Configuration](./ove ## OTLP configuration -OTLP is the [OpenTelemetry protocol](https://opentelemetry.io/docs/specs/otel/protocol/), and is the recommended protocol for transmitting telemetry, including traces, to Datadog. +[OpenTelemetry protocol (OTLP)](https://opentelemetry.io/docs/specs/otel/protocol/) is the recommended protocol for transmitting telemetry, including traces, to Datadog. To setup traces to Datadog via OTLP, you must do the following: From c91a2338af39d537521839375004151247e85027 Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Fri, 25 Oct 2024 09:49:22 +0100 Subject: [PATCH 11/35] Apply suggestions from code review Co-authored-by: Edward Huang --- .changesets/fix_bryn_datadog_agent_sampling.md | 18 +++++++++--------- ...g_upstream_sampling_decision_propagation.md | 7 +++---- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.changesets/fix_bryn_datadog_agent_sampling.md b/.changesets/fix_bryn_datadog_agent_sampling.md index 71d6306017..50c2d6997d 100644 --- a/.changesets/fix_bryn_datadog_agent_sampling.md +++ b/.changesets/fix_bryn_datadog_agent_sampling.md @@ -1,10 +1,10 @@ -### Add `preview_datadog_agent_sampling` ([PR #6017](https://github.com/apollographql/router/pull/6017)) +### Enable accurate Datadog APM metrics ([PR #6017](https://github.com/apollographql/router/pull/6017)) -The sampler option in the `telemetry.exporters.tracing.common.sampler` is not datadog aware. +The router supports a new preview feature, the `preview_datadog_agent_sampling` option, to enable sending all spans to the Datadog Agent so APM metrics and views are accurate. -To get accurate APM metrics all spans must be sent to the datadog agent with a `psr` or `sampling.priority` attribute set appropriately to record the sampling decision. +Previously, the sampler option in `telemetry.exporters.tracing.common.sampler` wasn't Datadog-aware. To get accurate Datadog APM metrics, all spans must be sent to the Datadog Agent with a `psr` or `sampling.priority` attribute set appropriately to record the sampling decision. -`preview_datadog_agent_sampling` option in the router.yaml enables this behavior and should be used when exporting to the datadog agent via OTLP or datadog native. +The `preview_datadog_agent_sampling` option enables accurate Datadog APM metrics. It should be used when exporting to the Datadog Agent, via OTLP or Datadog-native. ```yaml telemetry: @@ -19,16 +19,16 @@ telemetry: ``` -By using these options, you can decrease your Datadog bill as you will only be sending a percentage of spans from the Datadog agent to datadog. +Using these options can decrease your Datadog bill, because you will be sending only a percentage of spans from the Datadog Agent to Datadog. > [!IMPORTANT] > Users must enable `preview_datadog_agent_sampling` to get accurate APM metrics. Users that have been using recent versions of the router will have to modify their configuration to retain full APM metrics. > [!IMPORTANT] -> The Router does not support [`in-agent` ingestion control](https://docs.datadoghq.com/tracing/trace_pipeline/ingestion_mechanisms/?tab=java#in-the-agent). -> Configuring `traces_per_second` in the Datadog agent will NOT dynamically adjust the Router's sampling rate to meet the target rate. +> The router doesn't support [`in-agent` ingestion control](https://docs.datadoghq.com/tracing/trace_pipeline/ingestion_mechanisms/?tab=java#in-the-agent). +> Configuring `traces_per_second` in the Datadog Agent won't dynamically adjust the router's sampling rate to meet the target rate. > [!IMPORTANT] -> Sending all spans to the datadog agent may require that you tweak the `batch_processor` settings in your exporter config. This applies to both OTLP and the Datadog native exporter. +> Sending all spans to the Datadog Agent may require that you tweak the `batch_processor` settings in your exporter config. This applies to both OTLP and Datadog native exporters. -See the updated Datadog tracing documentation for more information on configuration options and their implications. \ No newline at end of file +Learn more by reading the [updated Datadog tracing documentation](https://apollographql.com/docs/router/configuration/telemetry/exporters/tracing/datadog) for more information on configuration options and their implications. \ No newline at end of file diff --git a/.changesets/fix_bryn_datadog_upstream_sampling_decision_propagation.md b/.changesets/fix_bryn_datadog_upstream_sampling_decision_propagation.md index c67d135689..d05f173528 100644 --- a/.changesets/fix_bryn_datadog_upstream_sampling_decision_propagation.md +++ b/.changesets/fix_bryn_datadog_upstream_sampling_decision_propagation.md @@ -1,7 +1,6 @@ -### Datadog priority sampling resolution is lost ([PR #6017](https://github.com/apollographql/router/pull/6017)) +### Fix transmitted header value for Datadog priority sampling resolution ([PR #6017](https://github.com/apollographql/router/pull/6017)) -Previously a `x-datadog-sampling-priority` of `-1` would be converted to `0` for downstream requests and `2` would be converted to `1`. -This means that when propagating to downstream services a value of USER_REJECT would be transmitted as AUTO_REJECT. +The router now transmits correct values of `x-datadog-sampling-priority` to downstream services. -This PR fixes this by ensuring that the `x-datadog-sampling-priority` is transmitted as is to downstream services. +Previously, an `x-datadog-sampling-priority` of `-1` was incorrectly converted to `0` for downstream requests, and `2` was incorrectly converted to `1`. When propagating to downstream services, this resulted in values of `USER_REJECT` being incorrectly transmitted as `AUTO_REJECT`. From 98624c70afa5b2bbe5e7c2cd8f9def02127d9fff Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Fri, 25 Oct 2024 09:50:48 +0100 Subject: [PATCH 12/35] Apply suggestions from code review Co-authored-by: Edward Huang --- .../telemetry/exporters/tracing/datadog.mdx | 58 ++++++++++--------- .../telemetry/exporters/tracing/overview.mdx | 22 ++++--- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx index f81989500c..27c41b3579 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/datadog.mdx @@ -16,7 +16,7 @@ For general tracing configuration, refer to [Router Tracing Configuration](./ove To setup traces to Datadog via OTLP, you must do the following: -- Modify the default configuration of the Datadog Agent to accept OTLP traces submitted to it by the router. +- Modify the default configuration of the Datadog Agent to accept OTLP traces from the router. - Configure the router to send traces to the configured Datadog Agent. ### Datadog Agent configuration @@ -35,14 +35,14 @@ For additional Datadog Agent configuration details, review Datadog's [Enabling O ### Router configuration -To configure the router, enable the [OTLP exporter](./otlp) and set `endpoint: `. For example: +To configure the router, enable the [OTLP exporter](/router/configuration/telemetry/exporters/tracing/otlp) and set `endpoint: `. For example: ```yaml title="router.yaml" telemetry: exporters: tracing: common: - # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + # Configured to forward 10 percent of spans from the Datadog Agent to Datadog. Experiment to find a value that is good for you. preview_datadog_agent_sampling: true sampler: 0.1 @@ -56,35 +56,37 @@ telemetry: max_concurrent_exports: 100 ``` -Adjusting the `sampler` will allow you to control the sampling decisions that the router will make on its own and decrease the rate at which you sample, which can have a direct impact on your Datadog bill. +Adjusting the `sampler` controls the sampling decisions that the router makes on its own and decreases the rate at which you sample. Your sample rate can have a direct impact on your Datadog bill. -Depending on the volume of spans being created in a router instance, it will be necessary to adjust the `batch_processor` settings in your `exporter` config. If this is necessary, you will see warning messages from the router regarding the batch span processor. This applies to both OTLP and the Datadog native exporter. +If you see warning messages from the router regarding the batch span processor, you may need to adjust your `batch_processor` settings in your `exporter` config to match the volume of spans being created in a router instance. This applies to both OTLP and the Datadog native exporters. ### Enabling Datadog Agent sampling -The Datadog APM view relies on traces to generate metrics. For this to be accurate 100% of requests must be sampled and sent to the Datadog agent. -To prevent ALL traces from then being sent to Datadog, you must set `preview_datadog_agent_sampling` to `true` and adjust the `sampler` to the desired percentage of traces to be sent to Datadog. +The Datadog APM view relies on traces to generate metrics. For these metrics to be accurate, all requests must be sampled and sent to the Datadog agent. +To prevent all traces from being sent to Datadog, in your router you must set `preview_datadog_agent_sampling` to `true` and adjust the `sampler` to the desired percentage of traces to be sent to Datadog. ```yaml title="router.yaml" telemetry: exporters: tracing: common: - # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + # Configured to forward 10 percent of spans from the Datadog Agent to Datadog. Experiment to find a value that is good for you. sampler: 0.1 preview_datadog_agent_sampling: true ``` - The Router does not support [`in-agent` ingestion control](https://docs.datadoghq.com/tracing/trace_pipeline/ingestion_mechanisms/?tab=java#in-the-agent). - Configuring `traces_per_second` in the Datadog agent will not dynamically adjust the Router's sampling rate to meet the target rate. - - - Using `preview_datadog_agent_sampling` will send ALL spans to the Datadog agent. This will have an impact on the resource usage and performance of both the Router and Datadog agent. + + - The router doesn't support [`in-agent` ingestion control](https://docs.datadoghq.com/tracing/trace_pipeline/ingestion_mechanisms/?tab=java#in-the-agent). + + - Configuring `traces_per_second` in the Datadog Agent will not dynamically adjust the router's sampling rate to meet the target rate. + + - Using `preview_datadog_agent_sampling` will send _all_ spans to the Datadog Agent. This will have an impact on the resource usage and performance of both the router and Datadog Agent. + ### Enabling log correlation @@ -118,7 +120,7 @@ telemetry: exporters: tracing: common: - # Only 10 percent of spans will be forwarded from the Datadog agent to Datadog. Experiment to find a value that is good for you! + # Configured to forward 10 percent of spans from the Datadog Agent to Datadog. Experiment to find a value that is good for you. preview_datadog_agent_sampling: true sampler: 0.1 @@ -313,18 +315,17 @@ telemetry: ## Sampler configuration -When using Datadog you will need to take into consideration if you want to use the Datadog APM view or rely on OTLP metrics to gain insight into the Router's performance. -The Datadog APM view is driven by traces, and for this to be accurate 100% of requests must be sampled and sent to the Datadog agent. +When using Datadog to gain insight into your router's performance, you need to decide whether to use the Datadog APM view or rely on OTLP metrics. +The Datadog APM view is driven by traces. In order for this view to be accurate, all requests must be sampled and sent to the Datadog Agent. -Tracing is expensive both in terms of APM costs but also Router performance, and typically you will want to set the `sampler` to a low value ion production environments. -However, this will mean that the APM view will only show a small percentage of traces. +Tracing is expensive both in terms of APM costs and router performance, so you typically will want to set the `sampler` to sample at low rates in production environments. +This, however, impacts the APM view, which will show only a small percentage of traces. -Datadog Agent sampling is a mode where ALL traces are sent to the Datadog agent, but only a percentage of them are forwarded to Datadog. This makes the APM view accurate while keeping costs low -at the cost of the Router having an effective sample rate of 100% under the hood. +To mitigate this, you can use Datadog Agent sampling mode, where _all_ traces are sent to the Datadog Agent but only a percentage of them are forwarded to Datadog. This keeps the APM view accurate while lowering costs. Note that the router will incur a performance cost of having an effective sample rate of 100%. Use the following guidelines on how to configure the `sampler` and `preview_datadog_agent_sampling` to get the desired behavior: -**I want the APM view to show metrics for 100% of traffic, and I am OK with the performance impact on the Router.** +**I want the APM view to show metrics for 100% of traffic, and I am OK with the performance impact on the router.** Set `preview_datadog_agent_sampling` to `true` and adjust the `sampler` to the desired percentage of traces to be sent to Datadog. @@ -339,9 +340,9 @@ telemetry: sampler: 0.1 ``` -**I want the Datadog agent to be in control of the percentage of traces sent to Datadog.** +**I want the Datadog Agent to be in control of the percentage of traces sent to Datadog.** -Use the Datadog agent `probabalistic_sampling` option sampler and set the `sampler` to `always_on` to allow the Datadog agent to control the sampling rate. +Use the Datadog Agent's `probabalistic_sampling` option sampler and set the `sampler` to `always_on` to allow the agent to control the sampling rate. Router config: ```yaml title="router.yaml" @@ -406,11 +407,14 @@ telemetry: sampler: 0.1 ``` - +Using `preview_datadog_agent_sampling` will send _all_ spans to the Datadog Agent, but only the percentage of traces configured by the `sampler` will be forwarded to Datadog. This means that your APM view will be accurate, but it will incur performance and resource usage costs for both the router and Datadog Agent to send and receive all spans. - Using `preview_datadog_agent_sampling` will send ALL spans to the Datadog agent, but only the `sampler` percentage of them will be forwarded to Datadog. This means that your APM view will be correct at the cost of - the Router sending more spans to the Datadog agent. This will have an impact on the resource usage and performance of both the Router and Datadog agent. +If your use case allows your APM view to show only a subset of traces, then you can set `preview_datadog_agent_sampling` to `false`. You should alternatively rely on OTLP metrics to gain insight into the router's performance. - If you are OK with your APM view only showing a subset of traces, then you can leave `preview_datadog_agent_sampling` to `false`, however it is recommended to rely on OTLP metrics to gain insight into the Router's performance. + + +- The router doesn't support [`in-agent` ingestion control](https://docs.datadoghq.com/tracing/trace_pipeline/ingestion_mechanisms/?tab=java#in-the-agent). + +- Configuring `traces_per_second` in the Datadog Agent will not dynamically adjust the router's sampling rate to meet the target rate. \ No newline at end of file diff --git a/docs/source/configuration/telemetry/exporters/tracing/overview.mdx b/docs/source/configuration/telemetry/exporters/tracing/overview.mdx index d9663a8ec6..5a10bad2bc 100644 --- a/docs/source/configuration/telemetry/exporters/tracing/overview.mdx +++ b/docs/source/configuration/telemetry/exporters/tracing/overview.mdx @@ -111,10 +111,20 @@ telemetry: - `parent_based_sampler` enables clients to make the sampling decision. This guarantees that a trace that starts at a client will also have spans at the router. You may wish to disable it (setting `parent_based_sampler: false`) if your router is exposed directly to the internet. + + ### `preview_datadog_agent_sampling` -The Datadog APM view relies on traces to generate metrics. For this to be accurate 100% of requests must be sampled and sent to the Datadog agent. -To prevent ALL traces from then being sent to Datadog, you must set `preview_datadog_agent_sampling` to `true` and adjust the `sampler` to the desired percentage of traces to be sent to Datadog. + + + + + +Enable accurate Datadog APM views with the `preview_datadog_agent_sampling` option. + +The Datadog APM view relies on traces to generate metrics. For this to be accurate, all requests must be sampled and sent to the Datadog Agent. + +To both enable accurate APM views and prevent _all_ traces from being sent to Datadog, you must set `preview_datadog_agent_sampling` to `true` and adjust the `sampler` to the desired percentage of traces to be sent to Datadog. ```yaml title="router.yaml" telemetry: @@ -126,13 +136,7 @@ telemetry: preview_datadog_agent_sampling: true ``` - - The Router does not support [`in-agent` ingestion control](https://docs.datadoghq.com/tracing/trace_pipeline/ingestion_mechanisms/?tab=java#in-the-agent). - Configuring `traces_per_second` in the Datadog agent will not dynamically adjust the Router's sampling rate to meet the target rate. - - - Using `preview_datadog_agent_sampling` will send ALL spans to the Datadog agent. This will have an impact on the resource usage and performance of both the Router and Datadog agent. - +To learn more details and limitations about this option, go to [`preview_datadog_agent_sampling`](/router/configuration/telemetry/exporters/tracing/datadog#preview_datadog_agent_sampling) in DataDog trace exporter docs. ### `propagation` From 30ffb131f670d5c7147a7f9ddc25598946f1ed7c Mon Sep 17 00:00:00 2001 From: bryn Date: Tue, 10 Dec 2024 11:20:17 +0000 Subject: [PATCH 13/35] Improve datadog and otlp sampling tests We made some weird configuration previously to force particular behaviour which when looked at subsequently didn't make any sense. In particular the otlp tests should have had otlp propagation with the datadog propagator also enabled. Also added a chack for the subgraph service to ensure it is correctly sampled. --- apollo-router/tests/common.rs | 20 +++- .../tests/integration/telemetry/datadog.rs | 113 ++++++------------ .../tests/integration/telemetry/otlp.rs | 84 +++++++++++-- 3 files changed, 121 insertions(+), 96 deletions(-) diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index 72010eeef9..2fa1d8a024 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -80,6 +80,7 @@ pub struct IntegrationTest { collect_stdio: Option<(tokio::sync::oneshot::Sender, regex::Regex)>, _subgraphs: wiremock::MockServer, telemetry: Telemetry, + extra_propagator: Telemetry, pub _tracer_provider_client: TracerProvider, pub _tracer_provider_subgraph: TracerProvider, @@ -105,11 +106,15 @@ struct TracedResponder { telemetry: Telemetry, subscriber_subgraph: Dispatch, subgraph_callback: Option>, + subgraph_context: Arc>>, } impl Respond for TracedResponder { fn respond(&self, request: &wiremock::Request) -> ResponseTemplate { let context = self.telemetry.extract_context(request); + *self.subgraph_context.lock().expect("lock poisoned") = + Some(context.span().span_context().clone()); + tracing_core::dispatcher::with_default(&self.subscriber_subgraph, || { let _context_guard = context.attach(); let span = info_span!("subgraph server"); @@ -285,11 +290,7 @@ impl Telemetry { context = context.with_remote_span_context(SpanContext::new( context.span().span_context().trace_id(), context.span().span_context().span_id(), - context - .span() - .span_context() - .trace_flags() - .with_sampled(true), + context.span().span_context().trace_flags(), true, state, )); @@ -316,7 +317,9 @@ impl IntegrationTest { pub async fn new( config: String, telemetry: Option, + extra_propagator: Option, responder: Option, + subgraph_context: Option>>>, collect_stdio: Option>, supergraph: Option, mut subgraph_overrides: HashMap, @@ -325,6 +328,7 @@ impl IntegrationTest { ) -> Self { let redis_namespace = Uuid::new_v4().to_string(); let telemetry = telemetry.unwrap_or_default(); + let extra_propagator = extra_propagator.unwrap_or_default(); let tracer_provider_client = telemetry.tracer_provider("client"); let subscriber_client = Self::dispatch(&tracer_provider_client); let tracer_provider_subgraph = telemetry.tracer_provider("subgraph"); @@ -355,7 +359,8 @@ impl IntegrationTest { ResponseTemplate::new(200).set_body_json(json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}))), telemetry: telemetry.clone(), subscriber_subgraph: Self::dispatch(&tracer_provider_subgraph), - subgraph_callback + subgraph_callback, + subgraph_context: subgraph_context.unwrap_or_default() }) .mount(&subgraphs) .await; @@ -390,6 +395,7 @@ impl IntegrationTest { subscriber_client, _tracer_provider_subgraph: tracer_provider_subgraph, telemetry, + extra_propagator, redis_namespace, log: log.unwrap_or_else(|| "error,apollo_router=info".to_owned()), } @@ -595,6 +601,7 @@ impl IntegrationTest { "router was not started, call `router.start().await; router.assert_started().await`" ); let telemetry = self.telemetry.clone(); + let extra_propagator = self.extra_propagator.clone(); let query = query.clone(); let url = format!("http://{}", self.bind_address()); @@ -624,6 +631,7 @@ impl IntegrationTest { let mut request = builder.json(&query).build().unwrap(); telemetry.inject_context(&mut request); + extra_propagator.inject_context(&mut request); match client.execute(request).await { Ok(response) => (span_id, response), Err(err) => { diff --git a/apollo-router/tests/integration/telemetry/datadog.rs b/apollo-router/tests/integration/telemetry/datadog.rs index 39757ee389..948ff9d165 100644 --- a/apollo-router/tests/integration/telemetry/datadog.rs +++ b/apollo-router/tests/integration/telemetry/datadog.rs @@ -8,9 +8,7 @@ use std::time::Duration; use anyhow::anyhow; use opentelemetry_api::trace::SpanContext; -use opentelemetry_api::trace::TraceContextExt; use opentelemetry_api::trace::TraceId; -use opentelemetry_api::Context; use serde_json::json; use serde_json::Value; use tower::BoxError; @@ -30,6 +28,8 @@ struct TraceSpec { measured_spans: HashSet<&'static str>, unmeasured_spans: HashSet<&'static str>, priority_sampled: Option<&'static str>, + subgraph_sampled: Option, + subgraph_context: Option>>>, // Not the metrics but the otel attribute no_priority_sampled_attribute: Option, } @@ -40,19 +40,13 @@ async fn test_no_sample() -> Result<(), BoxError> { return Ok(()); } let context = std::sync::Arc::new(std::sync::Mutex::new(None)); - let context_clone = context.clone(); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Datadog) .config(include_str!("fixtures/datadog_no_sample.router.yaml")) .responder(ResponseTemplate::new(200).set_body_json( json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), )) - .subgraph_callback(Box::new(move || { - let context = Context::current(); - let span = context.span(); - let span_context = span.span_context(); - *context_clone.lock().expect("poisoned") = Some(span_context.clone()); - })) + .subgraph_context(context.clone()) .build() .await; @@ -63,14 +57,12 @@ async fn test_no_sample() -> Result<(), BoxError> { let (_id, result) = router.execute_untraced_query(&query, None).await; router.graceful_shutdown().await; assert!(result.status().is_success()); - let context = context - .lock() - .expect("poisoned") - .as_ref() - .expect("state") - .clone(); - assert!(context.is_sampled()); - assert_eq!(context.trace_state().get("psr"), Some("0")); + let context = context.lock().expect("poisoned"); + assert!(!context.as_ref().unwrap().is_sampled()); + assert_eq!( + context.as_ref().unwrap().trace_state().get("psr"), + Some("0") + ); Ok(()) } @@ -82,7 +74,6 @@ async fn test_sampling_datadog_agent_disabled() -> Result<(), BoxError> { return Ok(()); } let context = std::sync::Arc::new(std::sync::Mutex::new(None)); - let context_clone = context.clone(); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Datadog) .config(include_str!( @@ -91,12 +82,7 @@ async fn test_sampling_datadog_agent_disabled() -> Result<(), BoxError> { .responder(ResponseTemplate::new(200).set_body_json( json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), )) - .subgraph_callback(Box::new(move || { - let context = Context::current(); - let span = context.span(); - let span_context = span.span_context(); - *context_clone.lock().expect("poisoned") = Some(span_context.clone()); - })) + .subgraph_context(context.clone()) .build() .await; @@ -107,20 +93,14 @@ async fn test_sampling_datadog_agent_disabled() -> Result<(), BoxError> { let (id, result) = router.execute_untraced_query(&query, None).await; router.graceful_shutdown().await; assert!(result.status().is_success()); - let _context = context - .lock() - .expect("poisoned") - .as_ref() - .expect("state") - .clone(); - tokio::time::sleep(Duration::from_secs(2)).await; + TraceSpec::builder() .services([].into()) + .subgraph_sampled(false) .build() .validate_trace(id) .await?; - Ok(()) } @@ -130,19 +110,13 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { return Ok(()); } let context = std::sync::Arc::new(std::sync::Mutex::new(None)); - let context_clone = context.clone(); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Datadog) .config(include_str!("fixtures/datadog.router.yaml")) .responder(ResponseTemplate::new(200).set_body_json( json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), )) - .subgraph_callback(Box::new(move || { - let context = Context::current(); - let span = context.span(); - let span_context = span.span_context(); - *context_clone.lock().expect("poisoned") = Some(span_context.clone()); - })) + .subgraph_context(context.clone()) .build() .await; @@ -151,41 +125,41 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { // Parent based sampling. psr MUST be populated with the value that we pass in. test_psr( - &context, &mut router, Some("-1"), TraceSpec::builder() - .services(["client", "router", "subgraph"].into()) + .services(["client", "router"].into()) + .subgraph_sampled(false) .priority_sampled("-1") .build(), ) .await?; test_psr( - &context, &mut router, Some("0"), TraceSpec::builder() - .services(["client", "router", "subgraph"].into()) + .services(["client", "router"].into()) + .subgraph_sampled(false) .priority_sampled("0") .build(), ) .await?; test_psr( - &context, &mut router, Some("1"), TraceSpec::builder() .services(["client", "router", "subgraph"].into()) + .subgraph_sampled(true) .priority_sampled("1") .build(), ) .await?; test_psr( - &context, &mut router, Some("2"), TraceSpec::builder() .services(["client", "router", "subgraph"].into()) + .subgraph_sampled(true) .priority_sampled("2") .build(), ) @@ -193,11 +167,11 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { // No psr was passed in the router is free to set it. This will be 1 as we are going to sample here. test_psr( - &context, &mut router, None, TraceSpec::builder() .services(["client", "router", "subgraph"].into()) + .subgraph_sampled(true) .priority_sampled("1") .build(), ) @@ -214,19 +188,13 @@ async fn test_priority_sampling_propagated_otel_request() -> Result<(), BoxError return Ok(()); } let context = std::sync::Arc::new(std::sync::Mutex::new(None)); - let context_clone = context.clone(); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Otlp { endpoint: None }) .config(include_str!("fixtures/datadog.router.yaml")) .responder(ResponseTemplate::new(200).set_body_json( json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), )) - .subgraph_callback(Box::new(move || { - let context = Context::current(); - let span = context.span(); - let span_context = span.span_context(); - *context_clone.lock().expect("poisoned") = Some(span_context.clone()); - })) + .subgraph_context(context.clone()) .build() .await; @@ -261,7 +229,6 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { return Ok(()); } let context = std::sync::Arc::new(std::sync::Mutex::new(None)); - let context_clone = context.clone(); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Datadog) .config(include_str!( @@ -270,12 +237,7 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { .responder(ResponseTemplate::new(200).set_body_json( json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), )) - .subgraph_callback(Box::new(move || { - let context = Context::current(); - let span = context.span(); - let span_context = span.span_context(); - *context_clone.lock().expect("poisoned") = Some(span_context.clone()); - })) + .subgraph_context(context.clone()) .build() .await; @@ -284,7 +246,6 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { // The router will ignore the upstream PSR as parent based sampling is disabled. test_psr( - &context, &mut router, Some("-1"), TraceSpec::builder() @@ -294,7 +255,6 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { ) .await?; test_psr( - &context, &mut router, Some("0"), TraceSpec::builder() @@ -304,7 +264,6 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { ) .await?; test_psr( - &context, &mut router, Some("1"), TraceSpec::builder() @@ -314,7 +273,6 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { ) .await?; test_psr( - &context, &mut router, Some("2"), TraceSpec::builder() @@ -325,7 +283,6 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { .await?; test_psr( - &context, &mut router, None, TraceSpec::builder() @@ -341,7 +298,6 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { } async fn test_psr( - context: &Arc>>, router: &mut IntegrationTest, psr: Option<&str>, trace_spec: TraceSpec, @@ -355,19 +311,7 @@ async fn test_psr( let (id, result) = router .execute_query_with_headers(&query, headers.into_iter().collect()) .await; - assert!(result.status().is_success()); - let context = context - .lock() - .expect("poisoned") - .as_ref() - .expect("state") - .clone(); - - assert_eq!( - context.trace_state().get("psr"), - trace_spec.priority_sampled - ); trace_spec.validate_trace(id).await?; Ok(()) } @@ -823,6 +767,19 @@ impl TraceSpec { tokio::time::sleep(Duration::from_millis(100)).await; } self.find_valid_trace(&url).await?; + + if let Some(subgraph_context) = &self.subgraph_context { + let subgraph_context = subgraph_context.lock().expect("poisoned"); + let subgraph_span_context = subgraph_context.as_ref().expect("state").clone(); + + assert_eq!( + subgraph_span_context.trace_state().get("psr"), + self.priority_sampled + ); + if let Some(sampled) = self.subgraph_sampled { + assert_eq!(subgraph_span_context.is_sampled(), sampled); + } + } Ok(()) } diff --git a/apollo-router/tests/integration/telemetry/otlp.rs b/apollo-router/tests/integration/telemetry/otlp.rs index 0ba9178cec..928eae1a4f 100644 --- a/apollo-router/tests/integration/telemetry/otlp.rs +++ b/apollo-router/tests/integration/telemetry/otlp.rs @@ -2,9 +2,12 @@ extern crate core; use std::collections::HashMap; use std::collections::HashSet; +use std::sync::Arc; +use std::sync::Mutex; use std::time::Duration; use anyhow::anyhow; +use opentelemetry_api::trace::SpanContext; use opentelemetry_api::trace::TraceId; use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceResponse; use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceResponse; @@ -67,6 +70,7 @@ async fn test_basic() -> Result<(), BoxError> { ] .into(), ) + .subgraph_sampled(true) .build() .validate_trace(id, &mock_server) .await?; @@ -106,6 +110,7 @@ async fn test_otlp_request_with_datadog_propagator() -> Result<(), BoxError> { Spec::builder() .services(["client", "router", "subgraph"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build() .validate_trace(id, &mock_server) .await?; @@ -136,6 +141,7 @@ async fn test_otlp_request_with_datadog_propagator_no_agent() -> Result<(), BoxE let (id, _) = router.execute_query(&query).await; Spec::builder() .services(["client", "router", "subgraph"].into()) + .subgraph_sampled(true) .build() .validate_trace(id, &mock_server) .await?; @@ -169,6 +175,7 @@ async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( Spec::builder() .services(["client", "router", "subgraph"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build() .validate_trace(id, &mock_server) .await?; @@ -194,6 +201,7 @@ async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( Spec::builder() .services(["router"].into()) .priority_sampled("0") + .subgraph_sampled(true) .build() .validate_trace(id, &mock_server) .await?; @@ -214,6 +222,7 @@ async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( Spec::builder() .services(["router", "subgraph"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build() .validate_trace(id, &mock_server) .await?; @@ -254,6 +263,7 @@ async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( Spec::builder() .services(["router"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build() .validate_trace(id, &mock_server) .await?; @@ -281,6 +291,7 @@ async fn test_untraced_request_no_sample_datadog_agent() -> Result<(), BoxError> Spec::builder() .services(["router"].into()) .priority_sampled("0") + .subgraph_sampled(false) .build() .validate_trace(id, &mock_server) .await?; @@ -306,6 +317,7 @@ async fn test_untraced_request_sample_datadog_agent() -> Result<(), BoxError> { Spec::builder() .services(["router"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build() .validate_trace(id, &mock_server) .await?; @@ -352,13 +364,18 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { let mock_server = mock_otlp_server().await; let config = include_str!("fixtures/otlp_datadog_propagation.router.yaml") .replace("", &mock_server.uri()); + let context = Arc::new(Mutex::new(None)); let mut router = IntegrationTest::builder() // We're using datadog propagation as this is what we are trying to test. - .telemetry(Telemetry::Datadog) + .telemetry(Telemetry::Otlp { + endpoint: Some(format!("{}/v1/traces", mock_server.uri())), + }) + .extra_propagator(Telemetry::Datadog) .config(config) .responder(ResponseTemplate::new(200).set_body_json( json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), )) + .subgraph_context(context.clone()) .build() .await; @@ -367,41 +384,49 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { // Parent based sampling. psr MUST be populated with the value that we pass in. test_psr( + &context, &mut router, Some("-1"), Spec::builder() - .services(["router"].into()) + .services(["client", "router"].into()) .priority_sampled("-1") + .subgraph_sampled(false) .build(), &mock_server, ) .await?; test_psr( + &context, &mut router, Some("0"), Spec::builder() - .services(["router"].into()) + .services(["client", "router"].into()) .priority_sampled("0") + .subgraph_sampled(false) .build(), &mock_server, ) .await?; test_psr( + &context, &mut router, Some("1"), Spec::builder() - .services(["router"].into()) + .services(["client", "router", "subgraph"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build(), &mock_server, ) .await?; test_psr( + &context, &mut router, Some("2"), Spec::builder() - .services(["router"].into()) + .services(["client", "router", "subgraph"].into()) .priority_sampled("2") + .subgraph_sampled(true) .build(), &mock_server, ) @@ -409,11 +434,13 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { // No psr was passed in the router is free to set it. This will be 1 as we are going to sample here. test_psr( + &context, &mut router, None, Spec::builder() - .services(["router"].into()) + .services(["client", "router", "subgraph"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build(), &mock_server, ) @@ -432,12 +459,17 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { let mock_server = mock_otlp_server().await; let config = include_str!("fixtures/otlp_datadog_propagation_no_parent_sampler.router.yaml") .replace("", &mock_server.uri()); + let context = Arc::new(Mutex::new(None)); let mut router = IntegrationTest::builder() - .telemetry(Telemetry::Datadog) + .telemetry(Telemetry::Otlp { + endpoint: Some(format!("{}/v1/traces", mock_server.uri())), + }) + .extra_propagator(Telemetry::Datadog) .config(config) .responder(ResponseTemplate::new(200).set_body_json( json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), )) + .subgraph_context(context.clone()) .build() .await; @@ -446,52 +478,62 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { // The router will ignore the upstream PSR as parent based sampling is disabled. test_psr( + &context, &mut router, Some("-1"), Spec::builder() - .services(["router"].into()) + .services(["client", "router", "subgraph"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build(), &mock_server, ) .await?; test_psr( + &context, &mut router, Some("0"), Spec::builder() - .services(["router"].into()) + .services(["client", "router", "subgraph"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build(), &mock_server, ) .await?; test_psr( + &context, &mut router, Some("1"), Spec::builder() - .services(["router"].into()) + .services(["client", "router", "subgraph"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build(), &mock_server, ) .await?; test_psr( + &context, &mut router, Some("2"), Spec::builder() - .services(["router"].into()) + .services(["client", "router", "subgraph"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build(), &mock_server, ) .await?; test_psr( + &context, &mut router, None, Spec::builder() - .services(["router"].into()) + .services(["client", "router", "subgraph"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build(), &mock_server, ) @@ -503,6 +545,7 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { } async fn test_psr( + context: &Arc>>, router: &mut IntegrationTest, psr: Option<&str>, trace_spec: Spec, @@ -519,12 +562,14 @@ async fn test_psr( .await; assert!(result.status().is_success()); + trace_spec.validate_trace(id, mock_server).await?; Ok(()) } #[derive(buildstructor::Builder)] struct Spec { + subgraph_context: Option>>>, operation_name: Option, version: Option, services: HashSet<&'static str>, @@ -532,6 +577,7 @@ struct Spec { measured_spans: HashSet<&'static str>, unmeasured_spans: HashSet<&'static str>, priority_sampled: Option<&'static str>, + subgraph_sampled: Option, } impl Spec { @@ -544,6 +590,20 @@ impl Spec { tokio::time::sleep(Duration::from_millis(100)).await; } self.find_valid_trace(id, mock_server).await?; + + if let Some(subgraph_context) = &self.subgraph_context { + let subgraph_context = subgraph_context.lock().expect("poisoned"); + let subgraph_span_context = subgraph_context.as_ref().expect("state").clone(); + + assert_eq!( + subgraph_span_context.trace_state().get("psr"), + self.priority_sampled + ); + if let Some(sampled) = self.subgraph_sampled { + assert_eq!(subgraph_span_context.is_sampled(), sampled); + } + } + Ok(()) } From 4490216c0b8b6b1aab9e1ded06c807e822984edb Mon Sep 17 00:00:00 2001 From: bryn Date: Tue, 10 Dec 2024 15:30:19 +0000 Subject: [PATCH 14/35] Lint and test fixes --- .../tests/integration/telemetry/otlp.rs | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/apollo-router/tests/integration/telemetry/otlp.rs b/apollo-router/tests/integration/telemetry/otlp.rs index 928eae1a4f..59dc15a518 100644 --- a/apollo-router/tests/integration/telemetry/otlp.rs +++ b/apollo-router/tests/integration/telemetry/otlp.rs @@ -158,10 +158,12 @@ async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( let mock_server = mock_otlp_server().await; let config = include_str!("fixtures/otlp_datadog_request_with_zipkin_propagator.router.yaml") .replace("", &mock_server.uri()); + let context = Arc::new(Mutex::new(None)); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Otlp { endpoint: Some(format!("{}/v1/traces", mock_server.uri())), }) + .subgraph_context(context.clone()) .config(&config) .build() .await; @@ -173,6 +175,7 @@ async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( let (id, _) = router.execute_query(&query).await; Spec::builder() + .subgraph_context(context.clone()) .services(["client", "router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) @@ -199,6 +202,7 @@ async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( let (_id, _) = router.execute_untraced_query(&query, Some(headers)).await; Spec::builder() + .subgraph_context(context.clone()) .services(["router"].into()) .priority_sampled("0") .subgraph_sampled(true) @@ -220,6 +224,7 @@ async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( let (_id, _) = router.execute_untraced_query(&query, Some(headers)).await; Spec::builder() + .subgraph_context(context.clone()) .services(["router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) @@ -241,6 +246,7 @@ async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( let (_id, _) = router.execute_untraced_query(&query, Some(headers)).await; Spec::builder() + .subgraph_context(context.clone()) .services(["router"].into()) .priority_sampled("0") .build() @@ -261,6 +267,7 @@ async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( let (_id, _) = router.execute_untraced_query(&query, Some(headers)).await; Spec::builder() + .subgraph_context(context.clone()) .services(["router"].into()) .priority_sampled("1") .subgraph_sampled(true) @@ -281,7 +288,12 @@ async fn test_untraced_request_no_sample_datadog_agent() -> Result<(), BoxError> let mock_server = mock_otlp_server().await; let config = include_str!("fixtures/otlp_datadog_agent_no_sample.router.yaml") .replace("", &mock_server.uri()); - let mut router = IntegrationTest::builder().config(&config).build().await; + let context = Arc::new(Mutex::new(None)); + let mut router = IntegrationTest::builder() + .subgraph_context(context.clone()) + .config(&config) + .build() + .await; router.start().await; router.assert_started().await; @@ -289,6 +301,7 @@ async fn test_untraced_request_no_sample_datadog_agent() -> Result<(), BoxError> let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); let (id, _) = router.execute_untraced_query(&query, None).await; Spec::builder() + .subgraph_context(context) .services(["router"].into()) .priority_sampled("0") .subgraph_sampled(false) @@ -307,7 +320,12 @@ async fn test_untraced_request_sample_datadog_agent() -> Result<(), BoxError> { let mock_server = mock_otlp_server().await; let config = include_str!("fixtures/otlp_datadog_agent_sample.router.yaml") .replace("", &mock_server.uri()); - let mut router = IntegrationTest::builder().config(&config).build().await; + let context = Arc::new(Mutex::new(None)); + let mut router = IntegrationTest::builder() + .subgraph_context(context.clone()) + .config(&config) + .build() + .await; router.start().await; router.assert_started().await; @@ -315,6 +333,7 @@ async fn test_untraced_request_sample_datadog_agent() -> Result<(), BoxError> { let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); let (id, _) = router.execute_untraced_query(&query, None).await; Spec::builder() + .subgraph_context(context.clone()) .services(["router"].into()) .priority_sampled("1") .subgraph_sampled(true) @@ -331,12 +350,14 @@ async fn test_untraced_request_sample_datadog_agent_unsampled() -> Result<(), Bo panic!("Error: test skipped because GraphOS is not enabled"); } let mock_server = mock_otlp_server().await; + let context = Arc::new(Mutex::new(None)); let config = include_str!("fixtures/otlp_datadog_agent_sample_no_sample.router.yaml") .replace("", &mock_server.uri()); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Otlp { endpoint: Some(format!("{}/v1/traces", mock_server.uri())), }) + .subgraph_context(context.clone()) .config(&config) .build() .await; @@ -349,6 +370,8 @@ async fn test_untraced_request_sample_datadog_agent_unsampled() -> Result<(), Bo Spec::builder() .services(["router"].into()) .priority_sampled("0") + .subgraph_sampled(false) + .subgraph_context(context.clone()) .build() .validate_trace(id, &mock_server) .await?; @@ -384,10 +407,10 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { // Parent based sampling. psr MUST be populated with the value that we pass in. test_psr( - &context, &mut router, Some("-1"), Spec::builder() + .subgraph_context(context.clone()) .services(["client", "router"].into()) .priority_sampled("-1") .subgraph_sampled(false) @@ -396,10 +419,10 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { ) .await?; test_psr( - &context, &mut router, Some("0"), Spec::builder() + .subgraph_context(context.clone()) .services(["client", "router"].into()) .priority_sampled("0") .subgraph_sampled(false) @@ -408,7 +431,6 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { ) .await?; test_psr( - &context, &mut router, Some("1"), Spec::builder() @@ -420,10 +442,10 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { ) .await?; test_psr( - &context, &mut router, Some("2"), Spec::builder() + .subgraph_context(context.clone()) .services(["client", "router", "subgraph"].into()) .priority_sampled("2") .subgraph_sampled(true) @@ -434,10 +456,10 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { // No psr was passed in the router is free to set it. This will be 1 as we are going to sample here. test_psr( - &context, &mut router, None, Spec::builder() + .subgraph_context(context.clone()) .services(["client", "router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) @@ -478,10 +500,10 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { // The router will ignore the upstream PSR as parent based sampling is disabled. test_psr( - &context, &mut router, Some("-1"), Spec::builder() + .subgraph_context(context.clone()) .services(["client", "router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) @@ -490,10 +512,10 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { ) .await?; test_psr( - &context, &mut router, Some("0"), Spec::builder() + .subgraph_context(context.clone()) .services(["client", "router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) @@ -502,10 +524,10 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { ) .await?; test_psr( - &context, &mut router, Some("1"), Spec::builder() + .subgraph_context(context.clone()) .services(["client", "router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) @@ -514,10 +536,10 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { ) .await?; test_psr( - &context, &mut router, Some("2"), Spec::builder() + .subgraph_context(context.clone()) .services(["client", "router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) @@ -527,10 +549,10 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { .await?; test_psr( - &context, &mut router, None, Spec::builder() + .subgraph_context(context.clone()) .services(["client", "router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) @@ -545,7 +567,6 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { } async fn test_psr( - context: &Arc>>, router: &mut IntegrationTest, psr: Option<&str>, trace_spec: Spec, From 7e17f313ded240e561b678b37bd4df7ef09debd0 Mon Sep 17 00:00:00 2001 From: Edward Huang Date: Wed, 11 Dec 2024 15:08:34 -0800 Subject: [PATCH 15/35] docs: remove redirects (#6419) --- docs/source/_redirects | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 docs/source/_redirects diff --git a/docs/source/_redirects b/docs/source/_redirects deleted file mode 100644 index be1bb526d6..0000000000 --- a/docs/source/_redirects +++ /dev/null @@ -1,10 +0,0 @@ -/managed-federation/overview /docs/federation/managed-federation/overview -/managed-federation/setup /docs/federation/managed-federation/setup -/managed-federation/federated-schema-checks /docs/federation/managed-federation/federated-schema-checks -/configuration/external/ /docs/router/customizations/coprocessor/ -/configuration/caching/ /docs/router/configuration/in-memory-caching/ -/development-workflow/* /docs/router/executing-operations/:splat -/configuration/spaceport* /docs/router/configuration/apollo-telemetry/ -/managed-federation/client%20awareness/ /docs/router/managed-federation/client-awareness/ -/configuration/logging /docs/router/configuration/telemetry/overview -/configuration/metrics /docs/router/configuration/telemetry/overview \ No newline at end of file From 232f40da48c23b01f59e30dd9e83678a6f1a26ca Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Thu, 12 Dec 2024 13:51:16 +0100 Subject: [PATCH 16/35] Remove the legacy query planner (#6418) --- .changesets/maint_simon_unbridge.md | 11 + .config/nextest.toml | 1 - CONTRIBUTING.md | 2 - Cargo.lock | 369 +---- apollo-router/Cargo.toml | 11 - apollo-router/examples/router.yaml | 2 - .../src/apollo_studio_interop/mod.rs | 45 +- .../src/apollo_studio_interop/tests.rs | 471 +----- apollo-router/src/axum_factory/tests.rs | 12 +- apollo-router/src/configuration/metrics.rs | 118 -- .../migrations/0032-remove-legacy-qp.yaml | 8 + apollo-router/src/configuration/mod.rs | 117 -- ...rics__test__experimental_mode_metrics.snap | 10 - ...cs__test__experimental_mode_metrics_2.snap | 10 - ...cs__test__experimental_mode_metrics_3.snap | 10 - ...nfiguration__tests__schema_generation.snap | 73 - ...rade_old_configuration@legacy_qp.yaml.snap | 7 + ...query_planner_parallelism_auto.router.yaml | 3 - ...ery_planner_parallelism_static.router.yaml | 3 - .../testdata/migrations/legacy_qp.yaml | 5 + apollo-router/src/error.rs | 178 +-- apollo-router/src/graphql/mod.rs | 118 +- apollo-router/src/graphql/response.rs | 2 +- apollo-router/src/lib.rs | 6 - .../cost_calculator/static_cost.rs | 4 +- .../src/plugins/include_subgraph_errors.rs | 12 +- apollo-router/src/plugins/telemetry/mod.rs | 4 +- apollo-router/src/plugins/test.rs | 14 +- .../src/plugins/traffic_shaping/mod.rs | 12 +- .../src/query_planner/bridge_query_planner.rs | 446 ++---- .../bridge_query_planner_pool.rs | 330 +---- .../query_planner/caching_query_planner.rs | 67 +- .../src/query_planner/dual_query_planner.rs | 182 --- apollo-router/src/query_planner/mod.rs | 2 - apollo-router/src/query_planner/plan.rs | 4 +- .../src/query_planner/plan_compare.rs | 1303 ----------------- apollo-router/src/query_planner/selection.rs | 7 - ...ridge_query_planner__tests__plan_root.snap | 4 +- apollo-router/src/query_planner/tests.rs | 2 +- apollo-router/src/router_factory.rs | 25 +- .../src/services/layers/query_analysis.rs | 2 +- apollo-router/src/services/query_planner.rs | 9 +- .../src/services/supergraph/service.rs | 8 +- ...nfig_update_query_planner_mode.router.yaml | 11 - ...g_update_reuse_query_fragments.router.yaml | 12 - .../tests/integration/query_planner.rs | 216 +-- .../query_planner/max_evaluated_plans.rs | 41 - apollo-router/tests/integration/redis.rs | 47 +- .../tests/integration/telemetry/metrics.rs | 18 - .../basic/configuration.yaml | 2 - .../basic/configuration2.yaml | 2 - .../progressive-override/basic/plan.json | 12 +- .../warmup/configuration.yaml | 2 - .../warmup/configuration2.yaml | 2 - .../progressive-override/warmup/plan.json | 12 +- apollo-router/tests/set_context.rs | 286 +--- ...tions___test_type_conditions_disabled.snap | 6 +- ...itions___test_type_conditions_enabled.snap | 30 +- ...ions_enabled_generate_query_fragments.snap | 30 +- ..._type_conditions_enabled_list_of_list.snap | 30 +- ...nditions_enabled_list_of_list_of_list.snap | 30 +- ...s_enabled_shouldnt_make_article_fetch.snap | 30 +- apollo-router/tests/type_conditions.rs | 36 +- .../source/reference/router/configuration.mdx | 78 - docs/source/routing/about-router.mdx | 2 - .../performance/query-planner-pools.mdx | 34 - .../query-planning/native-query-planner.mdx | 79 - fuzz/router.yaml | 1 - 68 files changed, 333 insertions(+), 4745 deletions(-) create mode 100644 .changesets/maint_simon_unbridge.md create mode 100644 apollo-router/src/configuration/migrations/0032-remove-legacy-qp.yaml delete mode 100644 apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics.snap delete mode 100644 apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap delete mode 100644 apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_3.snap create mode 100644 apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@legacy_qp.yaml.snap delete mode 100644 apollo-router/src/configuration/testdata/metrics/query_planner_parallelism_auto.router.yaml delete mode 100644 apollo-router/src/configuration/testdata/metrics/query_planner_parallelism_static.router.yaml create mode 100644 apollo-router/src/configuration/testdata/migrations/legacy_qp.yaml delete mode 100644 apollo-router/src/query_planner/dual_query_planner.rs delete mode 100644 apollo-router/src/query_planner/plan_compare.rs delete mode 100644 apollo-router/tests/integration/fixtures/query_planner_redis_config_update_query_planner_mode.router.yaml delete mode 100644 apollo-router/tests/integration/fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml delete mode 100644 docs/source/routing/performance/query-planner-pools.mdx delete mode 100644 docs/source/routing/query-planning/native-query-planner.mdx diff --git a/.changesets/maint_simon_unbridge.md b/.changesets/maint_simon_unbridge.md new file mode 100644 index 0000000000..6beb0908d3 --- /dev/null +++ b/.changesets/maint_simon_unbridge.md @@ -0,0 +1,11 @@ +### Remove the legacy query planner ([PR #6418](https://github.com/apollographql/router/pull/6418)) + +The legacy query planner has been removed in this release. In the previous release, Router 1.58, it was already no longer used by default but it was still available through the `experimental_query_planner_mode` configuration key. That key is now removed. + +Also removed are configuration keys which were only relevant to the legacy planner: + +* `supergraph.query_planning.experimental_parallelism`: the new planner can always use available parallelism. +* `supergraph.experimental_reuse_query_fragments`: this experimental algorithm that attempted to +reuse fragments from the original operation while forming subgraph requests is no longer present. Instead, by default new fragment definitions are generated based on the shape of the subgraph operation. + +By [@SimonSapin](https://github.com/SimonSapin) in https://github.com/apollographql/router/pull/6418 diff --git a/.config/nextest.toml b/.config/nextest.toml index f2c4ef3618..5c2a479dc3 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -118,7 +118,6 @@ or ( binary_id(=apollo-router::integration_tests) & test(=integration::redis::en or ( binary_id(=apollo-router::integration_tests) & test(=integration::redis::query_planner_redis_update_defer) ) or ( binary_id(=apollo-router::integration_tests) & test(=integration::redis::query_planner_redis_update_introspection) ) or ( binary_id(=apollo-router::integration_tests) & test(=integration::redis::query_planner_redis_update_query_fragments) ) -or ( binary_id(=apollo-router::integration_tests) & test(=integration::redis::query_planner_redis_update_reuse_query_fragments) ) or ( binary_id(=apollo-router::integration_tests) & test(=integration::redis::query_planner_redis_update_type_conditional_fetching) ) or ( binary_id(=apollo-router::integration_tests) & test(=integration::redis::test::connection_failure_blocks_startup) ) or ( binary_id(=apollo-router::integration_tests) & test(=integration::subgraph_response::test_invalid_error_locations_contains_negative_one_location) ) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 31ee69219e..c0e0f8af53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,8 +51,6 @@ Refer to [the README file](README.md) or run `cargo run --help` for more informa - `crates/apollo-router/src/main.rs`: the entry point for the executable -Some of the functionalities rely on the current Javascript / TypeScript implementation, provided by [apollo federation](https://github.com/apollographql/federation), which is exposed through the [federation router-bridge](https://github.com/apollographql/federation/tree/main/router-bridge). - ## Documentation Documentation for using and contributing to the Apollo Router Core is built using Gatsby diff --git a/Cargo.lock b/Cargo.lock index b6258b95be..5805ff09d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,7 +365,6 @@ dependencies = [ "reqwest", "rhai", "rmp", - "router-bridge", "rowan", "rstack", "rust-embed", @@ -374,7 +373,7 @@ dependencies = [ "rustls-pemfile", "ryu", "schemars", - "semver 1.0.23", + "semver", "serde", "serde_derive_default", "serde_json", @@ -1186,7 +1185,7 @@ dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", - "rustc_version 0.4.0", + "rustc_version", "tracing", ] @@ -1636,15 +1635,6 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" -[[package]] -name = "cmake" -version = "0.1.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" -dependencies = [ - "cc", -] - [[package]] name = "colorchoice" version = "1.0.1" @@ -2020,132 +2010,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" -[[package]] -name = "debugid" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" -dependencies = [ - "serde", - "uuid", -] - -[[package]] -name = "deno-proc-macro-rules" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c65c2ffdafc1564565200967edc4851c7b55422d3913466688907efd05ea26f" -dependencies = [ - "deno-proc-macro-rules-macros", - "proc-macro2", - "syn 2.0.90", -] - -[[package]] -name = "deno-proc-macro-rules-macros" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3047b312b7451e3190865713a4dd6e1f821aed614ada219766ebc3024a690435" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "deno_console" -version = "0.115.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ab05b798826985966deb29fc6773ed29570de2f2147a30c4289c7cdf635214" -dependencies = [ - "deno_core", -] - -[[package]] -name = "deno_core" -version = "0.200.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8ba264b90ceb6e95b39d82e674d8ecae86ca012f900338ea50d1a077d9d75fd" -dependencies = [ - "anyhow", - "bytes", - "deno_ops", - "futures", - "indexmap 1.9.3", - "libc", - "log", - "once_cell", - "parking_lot", - "pin-project", - "serde", - "serde_json", - "serde_v8", - "smallvec", - "sourcemap", - "tokio", - "url", - "v8", -] - -[[package]] -name = "deno_ops" -version = "0.78.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd1c83b1fd465ee0156f2917c9af9ca09fe2bf54052a2cae1a8dcbc7b89aefc" -dependencies = [ - "deno-proc-macro-rules", - "lazy-regex", - "once_cell", - "pmutil", - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "strum 0.25.0", - "strum_macros 0.25.3", - "syn 1.0.109", - "syn 2.0.90", - "thiserror", -] - -[[package]] -name = "deno_url" -version = "0.115.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20490fff3b0f8c176a815e26371ff23313ea7f39cd51057701524c5b6fc36f6c" -dependencies = [ - "deno_core", - "serde", - "urlpattern", -] - -[[package]] -name = "deno_web" -version = "0.146.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dc8dda6e1337d4739ae9e94d75521689824d82a7deb154a2972b6eedac64507" -dependencies = [ - "async-trait", - "base64-simd", - "deno_core", - "encoding_rs", - "flate2", - "serde", - "tokio", - "uuid", - "windows-sys 0.48.0", -] - -[[package]] -name = "deno_webidl" -version = "0.115.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73159d81053ead02e938b46d4bb7224c8e7cf25273ac16a250fb45bb09af7635" -dependencies = [ - "deno_core", -] - [[package]] name = "der" version = "0.7.9" @@ -2198,7 +2062,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version 0.4.0", + "rustc_version", "syn 2.0.90", ] @@ -2601,7 +2465,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", - "libz-ng-sys", "miniz_oxide", ] @@ -2724,7 +2587,7 @@ dependencies = [ "rustls", "rustls-native-certs", "rustls-webpki", - "semver 1.0.23", + "semver", "socket2 0.5.7", "tokio", "tokio-rustls", @@ -2743,16 +2606,6 @@ dependencies = [ "dunce", ] -[[package]] -name = "fslock" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57eafdd0c16f57161105ae1b98a1238f97645f2f588438b2949c99a2af9616bf" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "futures" version = "0.3.30" @@ -3665,12 +3518,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "if_chain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" - [[package]] name = "indexmap" version = "1.9.3" @@ -3985,29 +3832,6 @@ dependencies = [ "log", ] -[[package]] -name = "lazy-regex" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff63c423c68ea6814b7da9e88ce585f793c87ddd9e78f646970891769c8235d4" -dependencies = [ - "lazy-regex-proc_macros", - "once_cell", - "regex", -] - -[[package]] -name = "lazy-regex-proc_macros" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8edfc11b8f56ce85e207e62ea21557cfa09bb24a8f6b04ae181b086ff8611c22" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 1.0.109", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -4081,16 +3905,6 @@ dependencies = [ "threadpool", ] -[[package]] -name = "libz-ng-sys" -version = "1.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6409efc61b12687963e602df8ecf70e8ddacf95bc6576bcf16e3ac6328083c5" -dependencies = [ - "cmake", - "libc", -] - [[package]] name = "libz-sys" version = "1.1.18" @@ -4443,7 +4257,6 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", - "rand 0.8.5", ] [[package]] @@ -5104,17 +4917,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "pmutil" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - [[package]] name = "polling" version = "2.8.0" @@ -5762,30 +5564,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "router-bridge" -version = "0.6.4+v2.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bcc6f2aa0c619a4fb74ce271873a500f5640c257ca2e7aa8ea6be6226262855" -dependencies = [ - "anyhow", - "async-channel 1.9.0", - "deno_console", - "deno_core", - "deno_url", - "deno_web", - "deno_webidl", - "rand 0.8.5", - "serde", - "serde_json", - "thiserror", - "tokio", - "tower", - "tower-service", - "tracing", - "which", -] - [[package]] name = "router-fuzz" version = "0.0.0" @@ -5880,22 +5658,13 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] - [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.23", + "semver", ] [[package]] @@ -6091,27 +5860,12 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "serde" version = "1.0.215" @@ -6121,15 +5875,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde_bytes" -version = "0.11.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" -dependencies = [ - "serde", -] - [[package]] name = "serde_derive" version = "1.0.215" @@ -6233,22 +5978,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_v8" -version = "0.111.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "309b3060a9627882514f3a3ce3cc08ceb347a76aeeadc58f138c3f189cf88b71" -dependencies = [ - "bytes", - "derive_more", - "num-bigint", - "serde", - "serde_bytes", - "smallvec", - "thiserror", - "v8", -] - [[package]] name = "serde_yaml" version = "0.8.26" @@ -6425,22 +6154,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "sourcemap" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4cbf65ca7dc576cf50e21f8d0712d96d4fcfd797389744b7b222a85cdf5bd90" -dependencies = [ - "data-encoding", - "debugid", - "if_chain", - "rustc_version 0.2.3", - "serde", - "serde_json", - "unicode-id", - "url", -] - [[package]] name = "spin" version = "0.9.8" @@ -7408,47 +7121,6 @@ dependencies = [ "libc", ] -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - [[package]] name = "unicase" version = "2.7.0" @@ -7464,12 +7136,6 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" -[[package]] -name = "unicode-id" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -7524,19 +7190,6 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" -[[package]] -name = "urlpattern" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9bd5ff03aea02fa45b13a7980151fe45009af1980ba69f651ec367121a31609" -dependencies = [ - "derive_more", - "regex", - "serde", - "unic-ucd-ident", - "url", -] - [[package]] name = "utf-8" version = "0.7.6" @@ -7572,18 +7225,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "v8" -version = "0.74.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eedac634b8dd39b889c5b62349cbc55913780226239166435c5cf66771792ea" -dependencies = [ - "bitflags 1.3.2", - "fslock", - "once_cell", - "which", -] - [[package]] name = "valuable" version = "0.1.0" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index b2b139e9d7..890d9feb25 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -44,10 +44,6 @@ dhat-ad-hoc = ["dhat"] # alerted early when something is wrong instead of receiving an invalid result. failfast = [] -# "fake" feature to disable V8 usage when building on docs.rs -# See https://github.com/apollographql/federation-rs/pull/185 -docs_rs = ["router-bridge/docs_rs"] - # Enables the use of new telemetry features that are under development # and not yet ready for production use. telemetry_next = [] @@ -59,9 +55,6 @@ hyper_header_limits = [] # is set when ci builds take place. It allows us to disable some tests when CI is running on certain platforms. ci = [] -[package.metadata.docs.rs] -features = ["docs_rs"] - [dependencies] access-json = "0.1.0" anyhow = "1.0.86" @@ -191,10 +184,6 @@ rand = "0.8.5" rhai = { version = "1.19.0", features = ["sync", "serde", "internals"] } regex = "1.10.5" reqwest.workspace = true - -# note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.6.4+v2.9.3" - rust-embed = { version = "8.4.0", features = ["include-exclude"] } rustls = "0.21.12" rustls-native-certs = "0.6.3" diff --git a/apollo-router/examples/router.yaml b/apollo-router/examples/router.yaml index 6936d57e32..b544aab718 100644 --- a/apollo-router/examples/router.yaml +++ b/apollo-router/examples/router.yaml @@ -1,8 +1,6 @@ supergraph: listen: 0.0.0.0:4100 introspection: true - query_planning: - experimental_parallelism: auto # or any number plugins: experimental.expose_query_plan: true apollo-test.do_not_execute: true diff --git a/apollo-router/src/apollo_studio_interop/mod.rs b/apollo-router/src/apollo_studio_interop/mod.rs index 4499a9b557..b9877374ea 100644 --- a/apollo-router/src/apollo_studio_interop/mod.rs +++ b/apollo-router/src/apollo_studio_interop/mod.rs @@ -26,8 +26,7 @@ use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; use apollo_compiler::Node; use apollo_compiler::Schema; -use router_bridge::planner::ReferencedFieldsForType; -use router_bridge::planner::UsageReporting; +use serde::Deserialize; use serde::Serialize; use crate::json_ext::Object; @@ -162,14 +161,32 @@ impl AddAssign for AggregatedExtendedReferenceStats { } } -/// The result of the generate_usage_reporting function which contains a UsageReporting struct and -/// functions that allow comparison with another ComparableUsageReporting or UsageReporting object. -pub(crate) struct ComparableUsageReporting { - /// The UsageReporting fields - pub(crate) result: UsageReporting, +/// UsageReporting fields, that will be used to send stats to uplink/studio +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub(crate) struct UsageReporting { + /// The `stats_report_key` is a unique identifier derived from schema and query. + /// Metric data sent to Studio must be aggregated + /// via grouped key of (`client_name`, `client_version`, `stats_report_key`). + pub(crate) stats_report_key: String, + /// a list of all types and fields referenced in the query + #[serde(default)] + pub(crate) referenced_fields_by_type: HashMap, } -/// Generate a ComparableUsageReporting containing the stats_report_key (a normalized version of the operation signature) +/// A list of fields that will be resolved for a given type +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ReferencedFieldsForType { + /// names of the fields queried + #[serde(default)] + pub(crate) field_names: Vec, + /// whether the field is an interface + #[serde(default)] + pub(crate) is_interface: bool, +} + +/// Generate a UsageReporting containing the stats_report_key (a normalized version of the operation signature) /// and referenced fields of an operation. The document used to generate the signature and for the references can be /// different to handle cases where the operation has been filtered, but we want to keep the same signature. pub(crate) fn generate_usage_reporting( @@ -178,7 +195,7 @@ pub(crate) fn generate_usage_reporting( operation_name: &Option, schema: &Valid, normalization_algorithm: &ApolloSignatureNormalizationAlgorithm, -) -> ComparableUsageReporting { +) -> UsageReporting { let mut generator = UsageGenerator { signature_doc, references_doc, @@ -343,12 +360,10 @@ struct UsageGenerator<'a> { } impl UsageGenerator<'_> { - fn generate_usage_reporting(&mut self) -> ComparableUsageReporting { - ComparableUsageReporting { - result: UsageReporting { - stats_report_key: self.generate_stats_report_key(), - referenced_fields_by_type: self.generate_apollo_reporting_refs(), - }, + fn generate_usage_reporting(&mut self) -> UsageReporting { + UsageReporting { + stats_report_key: self.generate_stats_report_key(), + referenced_fields_by_type: self.generate_apollo_reporting_refs(), } } diff --git a/apollo-router/src/apollo_studio_interop/tests.rs b/apollo-router/src/apollo_studio_interop/tests.rs index 754951983c..1dd4d0fd89 100644 --- a/apollo-router/src/apollo_studio_interop/tests.rs +++ b/apollo-router/src/apollo_studio_interop/tests.rs @@ -1,54 +1,11 @@ use apollo_compiler::Schema; -use router_bridge::planner::PlanOptions; -use router_bridge::planner::Planner; -use router_bridge::planner::QueryPlannerConfig; use test_log::test; use super::*; use crate::Configuration; -macro_rules! assert_generated_report { - ($actual:expr) => { - // Field names need sorting - let mut result = $actual.result; - for ty in result.referenced_fields_by_type.values_mut() { - ty.field_names.sort(); - } - - insta::with_settings!({sort_maps => true, snapshot_suffix => "report"}, { - insta::assert_yaml_snapshot!(result); - }); - }; -} - -// Generate the signature and referenced fields using router-bridge to confirm that the expected value we used is correct. -// We can remove this when we no longer use the bridge but should keep the rust implementation verifications. -macro_rules! assert_bridge_results { - ($schema_str:expr, $query_str:expr) => { - let planner = Planner::::new( - $schema_str.to_string(), - QueryPlannerConfig::default(), - ) - .await - .unwrap(); - let mut plan = planner - .plan($query_str.to_string(), None, PlanOptions::default()) - .await - .unwrap(); - - // Field names need sorting - for ty in plan.usage_reporting.referenced_fields_by_type.values_mut() { - ty.field_names.sort(); - } - - insta::with_settings!({sort_maps => true, snapshot_suffix => "bridge"}, { - insta::assert_yaml_snapshot!(plan.usage_reporting); - }); - }; -} - -fn assert_expected_signature(actual: &ComparableUsageReporting, expected_sig: &str) { - assert_eq!(actual.result.stats_report_key, expected_sig); +fn assert_expected_signature(actual: &UsageReporting, expected_sig: &str) { + assert_eq!(actual.stats_report_key, expected_sig); } macro_rules! assert_extended_references { @@ -73,27 +30,12 @@ macro_rules! assert_enums_from_response { }; } -// Generate usage reporting with the same signature and refs doc, and with legacy normalization algorithm -fn generate_legacy( - doc: &ExecutableDocument, - operation_name: &Option, - schema: &Valid, -) -> ComparableUsageReporting { - generate_usage_reporting( - doc, - doc, - operation_name, - schema, - &ApolloSignatureNormalizationAlgorithm::Legacy, - ) -} - // Generate usage reporting with the same signature and refs doc, and with enhanced normalization algorithm fn generate_enhanced( doc: &ExecutableDocument, operation_name: &Option, schema: &Valid, -) -> ComparableUsageReporting { +) -> UsageReporting { generate_usage_reporting( doc, doc, @@ -140,413 +82,6 @@ fn enums_from_response( result } -#[test(tokio::test)] -async fn test_complex_query() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/complex_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("TransformedQuery".into()), &schema); - - assert_generated_report!(generated); - - // the router-bridge planner will throw errors on unused fragments/queries so we remove them here - let sanitised_query_str = include_str!("testdata/complex_query_sanitized.graphql"); - - assert_bridge_results!(schema_str, sanitised_query_str); -} - -#[test(tokio::test)] -async fn test_complex_references() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/complex_references_query.graphql"); - - let schema: Valid = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("Query".into()), &schema); - - assert_generated_report!(generated); - - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_basic_whitespace() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/named_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("MyQuery".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_anonymous_query() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/anonymous_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &None, &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_anonymous_mutation() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/anonymous_mutation.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &None, &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_anonymous_subscription() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str: &str = include_str!("testdata/subscription_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &None, &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_ordered_fields_and_variables() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/ordered_fields_and_variables_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("VariableScalarInputQuery".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_fragments() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/fragments_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("FragmentQuery".into()), &schema); - - assert_generated_report!(generated); - - // the router-bridge planner will throw errors on unused fragments/queries so we remove them here - let sanitised_query_str = r#"query FragmentQuery { - noInputQuery { - listOfBools - interfaceResponse { - sharedField - ... on InterfaceImplementation2 { - implementation2Field - } - ...bbbInterfaceFragment - ...aaaInterfaceFragment - ... { - ... on InterfaceImplementation1 { - implementation1Field - } - } - ... on InterfaceImplementation1 { - implementation1Field - } - } - unionResponse { - ... on UnionType2 { - unionType2Field - } - ... on UnionType1 { - unionType1Field - } - } - ...zzzFragment - ...aaaFragment - ...ZZZFragment - } - } - - fragment zzzFragment on EverythingResponse { - listOfInterfaces { - sharedField - } - } - - fragment ZZZFragment on EverythingResponse { - listOfInterfaces { - sharedField - } - } - - fragment aaaFragment on EverythingResponse { - listOfInterfaces { - sharedField - } - } - - fragment bbbInterfaceFragment on InterfaceImplementation2 { - sharedField - implementation2Field - } - - fragment aaaInterfaceFragment on InterfaceImplementation1 { - sharedField - }"#; - assert_bridge_results!(schema_str, sanitised_query_str); -} - -#[test(tokio::test)] -async fn test_directives() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/directives_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("DirectiveQuery".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_aliases() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/aliases_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("AliasQuery".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_inline_values() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/inline_values_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("InlineInputTypeQuery".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_root_type_fragment() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/root_type_fragment_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &None, &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_directive_arg_spacing() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/directive_arg_spacing_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &None, &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_operation_with_single_variable() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/operation_with_single_variable_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("QueryWithVar".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_operation_with_multiple_variables() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/operation_with_multiple_variables_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("QueryWithVars".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_field_arg_comma_or_space() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/field_arg_comma_or_space_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("QueryArgLength".into()), &schema); - - // enumInputQuery has a variable line length of 81, so it should be separated by spaces (which are converted from newlines - // in the original implementation). - // enumInputQuery has a variable line length of 80, so it should be separated by commas. - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_operation_arg_always_commas() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/operation_arg_always_commas_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("QueryArgLength".into()), &schema); - - // operation variables shouldn't ever be converted to spaces, since the line length check is only on field variables - // in the original implementation - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_comma_separator_always() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/comma_separator_always_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("QueryCommaEdgeCase".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_nested_fragments() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/nested_fragments_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("NestedFragmentQuery".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_mutation_space() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/mutation_space_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("Test_Mutation_Space".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_mutation_comma() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/mutation_comma_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("Test_Mutation_Comma".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_comma_lower_bound() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/comma_lower_bound_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("TestCommaLowerBound".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_comma_upper_bound() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/comma_upper_bound_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("TestCommaUpperBound".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - -#[test(tokio::test)] -async fn test_underscore() { - let schema_str = include_str!("testdata/schema_interop.graphql"); - let query_str = include_str!("testdata/underscore_query.graphql"); - - let schema = Schema::parse_and_validate(schema_str, "schema.graphql").unwrap(); - let doc = ExecutableDocument::parse(&schema, query_str, "query.graphql").unwrap(); - - let generated = generate_legacy(&doc, &Some("UnderscoreQuery".into()), &schema); - - assert_generated_report!(generated); - assert_bridge_results!(schema_str, query_str); -} - #[test(tokio::test)] async fn test_enhanced_uses_comma_always() { let schema_str = include_str!("testdata/schema_interop.graphql"); diff --git a/apollo-router/src/axum_factory/tests.rs b/apollo-router/src/axum_factory/tests.rs index efcf7e3ab3..0aced33790 100644 --- a/apollo-router/src/axum_factory/tests.rs +++ b/apollo-router/src/axum_factory/tests.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use std::io; use std::net::SocketAddr; -use std::num::NonZeroUsize; use std::pin::Pin; use std::str::FromStr; use std::sync::atomic::AtomicU32; @@ -2267,14 +2266,9 @@ async fn test_supergraph_timeout() { let schema = include_str!("..//testdata/minimal_supergraph.graphql"); let schema = Arc::new(Schema::parse(schema, &conf).unwrap()); - let planner = BridgeQueryPlannerPool::new( - Vec::new(), - schema.clone(), - conf.clone(), - NonZeroUsize::new(1).unwrap(), - ) - .await - .unwrap(); + let planner = BridgeQueryPlannerPool::new(schema.clone(), conf.clone()) + .await + .unwrap(); // we do the entire supergraph rebuilding instead of using `from_supergraph_mock_callback_and_configuration` // because we need the plugins to apply on the supergraph diff --git a/apollo-router/src/configuration/metrics.rs b/apollo-router/src/configuration/metrics.rs index 7241950fd7..4fcfd6c509 100644 --- a/apollo-router/src/configuration/metrics.rs +++ b/apollo-router/src/configuration/metrics.rs @@ -8,7 +8,6 @@ use opentelemetry_api::KeyValue; use paste::paste; use serde_json::Value; -use super::AvailableParallelism; use crate::metrics::meter_provider; use crate::uplink::license_enforcement::LicenseState; use crate::Configuration; @@ -47,9 +46,6 @@ impl Metrics { ); data.populate_license_instrument(license_state); data.populate_user_plugins_instrument(configuration); - data.populate_query_planner_experimental_parallelism(configuration); - data.populate_deno_or_rust_mode_instruments(configuration); - data.populate_legacy_fragment_usage(configuration); data.into() } @@ -497,85 +493,6 @@ impl InstrumentData { ), ); } - - pub(crate) fn populate_legacy_fragment_usage(&mut self, configuration: &Configuration) { - // Fragment generation takes precedence over fragment reuse. Only report when fragment reuse is *actually active*. - if configuration.supergraph.reuse_query_fragments == Some(true) - && !configuration.supergraph.generate_query_fragments - { - self.data.insert( - "apollo.router.config.reuse_query_fragments".to_string(), - (1, HashMap::new()), - ); - } - } - - pub(crate) fn populate_query_planner_experimental_parallelism( - &mut self, - configuration: &Configuration, - ) { - let query_planner_parallelism_config = configuration - .supergraph - .query_planning - .experimental_parallelism; - - if query_planner_parallelism_config != Default::default() { - let mut attributes = HashMap::new(); - attributes.insert( - "mode".to_string(), - if let AvailableParallelism::Auto(_) = query_planner_parallelism_config { - "auto" - } else { - "static" - } - .into(), - ); - self.data.insert( - "apollo.router.config.query_planning.parallelism".to_string(), - ( - configuration - .supergraph - .query_planning - .experimental_query_planner_parallelism() - .map(|n| { - #[cfg(test)] - { - // Set to a fixed number for snapshot tests - if let AvailableParallelism::Auto(_) = - query_planner_parallelism_config - { - return 8; - } - } - let as_usize: usize = n.into(); - let as_u64: u64 = as_usize.try_into().unwrap_or_default(); - as_u64 - }) - .unwrap_or_default(), - attributes, - ), - ); - } - } - - /// Populate metrics on the rollout of experimental Rust replacements of JavaScript code. - pub(crate) fn populate_deno_or_rust_mode_instruments(&mut self, configuration: &Configuration) { - let experimental_query_planner_mode = match configuration.experimental_query_planner_mode { - super::QueryPlannerMode::Legacy => "legacy", - super::QueryPlannerMode::Both => "both", - super::QueryPlannerMode::BothBestEffort => "both_best_effort", - super::QueryPlannerMode::New => "new", - super::QueryPlannerMode::NewBestEffort => "new_best_effort", - }; - - self.data.insert( - "apollo.router.config.experimental_query_planner_mode".to_string(), - ( - 1, - HashMap::from_iter([("mode".to_string(), experimental_query_planner_mode.into())]), - ), - ); - } } impl From for Metrics { @@ -608,9 +525,7 @@ mod test { use crate::configuration::metrics::InstrumentData; use crate::configuration::metrics::Metrics; - use crate::configuration::QueryPlannerMode; use crate::uplink::license_enforcement::LicenseState; - use crate::Configuration; #[derive(RustEmbed)] #[folder = "src/configuration/testdata/metrics"] @@ -628,8 +543,6 @@ mod test { let mut data = InstrumentData::default(); data.populate_config_instruments(yaml); - let configuration: Configuration = input.parse().unwrap(); - data.populate_query_planner_experimental_parallelism(&configuration); let _metrics: Metrics = data.into(); assert_non_zero_metrics_snapshot!(file_name); } @@ -683,35 +596,4 @@ mod test { let _metrics: Metrics = data.into(); assert_non_zero_metrics_snapshot!(); } - - #[test] - fn test_experimental_mode_metrics() { - let mut data = InstrumentData::default(); - data.populate_deno_or_rust_mode_instruments(&Configuration { - experimental_query_planner_mode: QueryPlannerMode::Both, - ..Default::default() - }); - let _metrics: Metrics = data.into(); - assert_non_zero_metrics_snapshot!(); - } - - #[test] - fn test_experimental_mode_metrics_2() { - let mut data = InstrumentData::default(); - // Default query planner value should still be reported - data.populate_deno_or_rust_mode_instruments(&Configuration::default()); - let _metrics: Metrics = data.into(); - assert_non_zero_metrics_snapshot!(); - } - - #[test] - fn test_experimental_mode_metrics_3() { - let mut data = InstrumentData::default(); - data.populate_deno_or_rust_mode_instruments(&Configuration { - experimental_query_planner_mode: QueryPlannerMode::New, - ..Default::default() - }); - let _metrics: Metrics = data.into(); - assert_non_zero_metrics_snapshot!(); - } } diff --git a/apollo-router/src/configuration/migrations/0032-remove-legacy-qp.yaml b/apollo-router/src/configuration/migrations/0032-remove-legacy-qp.yaml new file mode 100644 index 0000000000..281de2b607 --- /dev/null +++ b/apollo-router/src/configuration/migrations/0032-remove-legacy-qp.yaml @@ -0,0 +1,8 @@ +description: the legacy query planner was removed, together with relevant configuration keys experimental_query_planner_mode and supergraph.query_planning.experimental_parallelism +actions: + - type: delete + path: experimental_query_planner_mode + - type: delete + path: supergraph.experimental_reuse_query_fragments + - type: delete + path: supergraph.query_planning.experimental_parallelism diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index 8bdc97b52a..a201a647cb 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -162,10 +162,6 @@ pub struct Configuration { #[serde(default)] pub(crate) experimental_chaos: Chaos, - /// Set the query planner implementation to use. - #[serde(default)] - pub(crate) experimental_query_planner_mode: QueryPlannerMode, - /// Plugin configuration #[serde(default)] pub(crate) plugins: UserPlugins, @@ -197,40 +193,6 @@ impl PartialEq for Configuration { } } -/// Query planner modes. -#[derive(Clone, PartialEq, Eq, Default, Derivative, Serialize, Deserialize, JsonSchema)] -#[derivative(Debug)] -#[serde(rename_all = "snake_case")] -pub(crate) enum QueryPlannerMode { - /// Use the new Rust-based implementation. - /// - /// Raises an error at Router startup if the the new planner does not support the schema - /// (such as using legacy Apollo Federation 1) - New, - /// Use the old JavaScript-based implementation. - Legacy, - /// Use primarily the Javascript-based implementation, - /// but also schedule background jobs to run the Rust implementation and compare results, - /// logging warnings if the implementations disagree. - /// - /// Raises an error at Router startup if the the new planner does not support the schema - /// (such as using legacy Apollo Federation 1) - Both, - /// Use primarily the Javascript-based implementation, - /// but also schedule on a best-effort basis background jobs - /// to run the Rust implementation and compare results, - /// logging warnings if the implementations disagree. - /// - /// Falls back to `legacy` with a warning - /// if the the new planner does not support the schema - /// (such as using legacy Apollo Federation 1) - BothBestEffort, - /// Use the new Rust-based implementation but fall back to the legacy one - /// for supergraph schemas composed with legacy Apollo Federation 1. - #[default] - NewBestEffort, -} - impl<'de> serde::Deserialize<'de> for Configuration { fn deserialize(deserializer: D) -> Result where @@ -256,7 +218,6 @@ impl<'de> serde::Deserialize<'de> for Configuration { experimental_chaos: Chaos, batching: Batching, experimental_type_conditioned_fetching: bool, - experimental_query_planner_mode: QueryPlannerMode, } let mut ad_hoc: AdHocConfiguration = serde::Deserialize::deserialize(deserializer)?; @@ -283,7 +244,6 @@ impl<'de> serde::Deserialize<'de> for Configuration { limits: ad_hoc.limits, experimental_chaos: ad_hoc.experimental_chaos, experimental_type_conditioned_fetching: ad_hoc.experimental_type_conditioned_fetching, - experimental_query_planner_mode: ad_hoc.experimental_query_planner_mode, plugins: ad_hoc.plugins, apollo_plugins: ad_hoc.apollo_plugins, batching: ad_hoc.batching, @@ -329,7 +289,6 @@ impl Configuration { uplink: Option, experimental_type_conditioned_fetching: Option, batching: Option, - experimental_query_planner_mode: Option, ) -> Result { let notify = Self::notify(&apollo_plugins)?; @@ -344,7 +303,6 @@ impl Configuration { persisted_queries: persisted_query.unwrap_or_default(), limits: operation_limits.unwrap_or_default(), experimental_chaos: chaos.unwrap_or_default(), - experimental_query_planner_mode: experimental_query_planner_mode.unwrap_or_default(), plugins: UserPlugins { plugins: Some(plugins), }, @@ -396,27 +354,6 @@ impl Configuration { ).build()) } - pub(crate) fn js_query_planner_config(&self) -> router_bridge::planner::QueryPlannerConfig { - router_bridge::planner::QueryPlannerConfig { - reuse_query_fragments: self.supergraph.reuse_query_fragments, - generate_query_fragments: Some(self.supergraph.generate_query_fragments), - incremental_delivery: Some(router_bridge::planner::IncrementalDeliverySupport { - enable_defer: Some(self.supergraph.defer_support), - }), - graphql_validation: false, - debug: Some(router_bridge::planner::QueryPlannerDebugConfig { - bypass_planner_for_single_subgraph: None, - max_evaluated_plans: self - .supergraph - .query_planning - .experimental_plans_limit - .or(Some(10000)), - paths_limit: self.supergraph.query_planning.experimental_paths_limit, - }), - type_conditioned_fetching: self.experimental_type_conditioned_fetching, - } - } - pub(crate) fn rust_query_planner_config( &self, ) -> apollo_federation::query_plan::query_planner::QueryPlannerConfig { @@ -475,7 +412,6 @@ impl Configuration { uplink: Option, batching: Option, experimental_type_conditioned_fetching: Option, - experimental_query_planner_mode: Option, ) -> Result { let configuration = Self { validated_yaml: Default::default(), @@ -486,7 +422,6 @@ impl Configuration { cors: cors.unwrap_or_default(), limits: operation_limits.unwrap_or_default(), experimental_chaos: chaos.unwrap_or_default(), - experimental_query_planner_mode: experimental_query_planner_mode.unwrap_or_default(), plugins: UserPlugins { plugins: Some(plugins), }, @@ -700,12 +635,6 @@ pub(crate) struct Supergraph { /// Default: false pub(crate) introspection: bool, - /// Enable reuse of query fragments - /// This feature is deprecated and will be removed in next release. - /// The config can only be set when the legacy query planner is explicitly enabled. - #[serde(rename = "experimental_reuse_query_fragments")] - pub(crate) reuse_query_fragments: Option, - /// Enable QP generation of fragments for subgraph requests /// Default: true pub(crate) generate_query_fragments: bool, @@ -731,25 +660,12 @@ const fn default_generate_query_fragments() -> bool { true } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] -#[serde(rename_all = "snake_case", untagged)] -pub(crate) enum AvailableParallelism { - Auto(Auto), - Fixed(NonZeroUsize), -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub(crate) enum Auto { Auto, } -impl Default for AvailableParallelism { - fn default() -> Self { - Self::Fixed(NonZeroUsize::new(1).expect("cannot fail")) - } -} - fn default_defer_support() -> bool { true } @@ -763,7 +679,6 @@ impl Supergraph { introspection: Option, defer_support: Option, query_planning: Option, - reuse_query_fragments: Option, generate_query_fragments: Option, early_cancel: Option, experimental_log_on_broken_pipe: Option, @@ -774,15 +689,6 @@ impl Supergraph { introspection: introspection.unwrap_or_else(default_graphql_introspection), defer_support: defer_support.unwrap_or_else(default_defer_support), query_planning: query_planning.unwrap_or_default(), - reuse_query_fragments: generate_query_fragments.and_then(|v| - if v { - if reuse_query_fragments.is_some_and(|v| v) { - // warn the user that both are enabled and it's overridden - tracing::warn!("Both 'generate_query_fragments' and 'experimental_reuse_query_fragments' are explicitly enabled, 'experimental_reuse_query_fragments' will be overridden to false"); - } - Some(false) - } else { reuse_query_fragments } - ), generate_query_fragments: generate_query_fragments .unwrap_or_else(default_generate_query_fragments), early_cancel: early_cancel.unwrap_or_default(), @@ -801,7 +707,6 @@ impl Supergraph { introspection: Option, defer_support: Option, query_planning: Option, - reuse_query_fragments: Option, generate_query_fragments: Option, early_cancel: Option, experimental_log_on_broken_pipe: Option, @@ -812,15 +717,6 @@ impl Supergraph { introspection: introspection.unwrap_or_else(default_graphql_introspection), defer_support: defer_support.unwrap_or_else(default_defer_support), query_planning: query_planning.unwrap_or_default(), - reuse_query_fragments: generate_query_fragments.and_then(|v| - if v { - if reuse_query_fragments.is_some_and(|v| v) { - // warn the user that both are enabled and it's overridden - tracing::warn!("Both 'generate_query_fragments' and 'experimental_reuse_query_fragments' are explicitly enabled, 'experimental_reuse_query_fragments' will be overridden to false"); - } - Some(false) - } else { reuse_query_fragments } - ), generate_query_fragments: generate_query_fragments .unwrap_or_else(default_generate_query_fragments), early_cancel: early_cancel.unwrap_or_default(), @@ -944,19 +840,6 @@ pub(crate) struct QueryPlanning { /// If cache warm up is configured, this will allow the router to keep a query plan created with /// the old schema, if it determines that the schema update does not affect the corresponding query pub(crate) experimental_reuse_query_plans: bool, - - /// Set the size of a pool of workers to enable query planning parallelism. - /// Default: 1. - pub(crate) experimental_parallelism: AvailableParallelism, -} - -impl QueryPlanning { - pub(crate) fn experimental_query_planner_parallelism(&self) -> io::Result { - match self.experimental_parallelism { - AvailableParallelism::Auto(Auto::Auto) => std::thread::available_parallelism(), - AvailableParallelism::Fixed(n) => Ok(n), - } - } } /// Cache configuration diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics.snap deleted file mode 100644 index 513298fd51..0000000000 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: apollo-router/src/configuration/metrics.rs -expression: "&metrics.non_zero()" ---- -- name: apollo.router.config.experimental_query_planner_mode - data: - datapoints: - - value: 1 - attributes: - mode: both diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap deleted file mode 100644 index e976e8391d..0000000000 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: apollo-router/src/configuration/metrics.rs -expression: "&metrics.non_zero()" ---- -- name: apollo.router.config.experimental_query_planner_mode - data: - datapoints: - - value: 1 - attributes: - mode: new_best_effort diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_3.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_3.snap deleted file mode 100644 index 34d0b7fe07..0000000000 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_3.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: apollo-router/src/configuration/metrics.rs -expression: "&metrics.non_zero()" ---- -- name: apollo.router.config.experimental_query_planner_mode - data: - datapoints: - - value: 1 - attributes: - mode: new 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 4ae8457f17..2713e63dc5 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 @@ -307,25 +307,6 @@ expression: "&schema" } ] }, - "Auto": { - "enum": [ - "auto" - ], - "type": "string" - }, - "AvailableParallelism": { - "anyOf": [ - { - "$ref": "#/definitions/Auto", - "description": "#/definitions/Auto" - }, - { - "format": "uint", - "minimum": 1.0, - "type": "integer" - } - ] - }, "BatchProcessorConfig": { "description": "Batch processor configuration", "properties": { @@ -4453,46 +4434,6 @@ expression: "&schema" ], "type": "object" }, - "QueryPlannerMode": { - "description": "Query planner modes.", - "oneOf": [ - { - "description": "Use the new Rust-based implementation.\n\nRaises an error at Router startup if the the new planner does not support the schema (such as using legacy Apollo Federation 1)", - "enum": [ - "new" - ], - "type": "string" - }, - { - "description": "Use the old JavaScript-based implementation.", - "enum": [ - "legacy" - ], - "type": "string" - }, - { - "description": "Use primarily the Javascript-based implementation, but also schedule background jobs to run the Rust implementation and compare results, logging warnings if the implementations disagree.\n\nRaises an error at Router startup if the the new planner does not support the schema (such as using legacy Apollo Federation 1)", - "enum": [ - "both" - ], - "type": "string" - }, - { - "description": "Use primarily the Javascript-based implementation, but also schedule on a best-effort basis background jobs to run the Rust implementation and compare results, logging warnings if the implementations disagree.\n\nFalls back to `legacy` with a warning if the the new planner does not support the schema (such as using legacy Apollo Federation 1)", - "enum": [ - "both_best_effort" - ], - "type": "string" - }, - { - "description": "Use the new Rust-based implementation but fall back to the legacy one for supergraph schemas composed with legacy Apollo Federation 1.", - "enum": [ - "new_best_effort" - ], - "type": "string" - } - ] - }, "QueryPlanning": { "additionalProperties": false, "description": "Query planning cache configuration", @@ -4501,10 +4442,6 @@ expression: "&schema" "$ref": "#/definitions/QueryPlanCache", "description": "#/definitions/QueryPlanCache" }, - "experimental_parallelism": { - "$ref": "#/definitions/AvailableParallelism", - "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.", @@ -6649,12 +6586,6 @@ expression: "&schema" "description": "Log a message if the client closes the connection before the response is sent. Default: false.", "type": "boolean" }, - "experimental_reuse_query_fragments": { - "default": null, - "description": "Enable reuse of query fragments This feature is deprecated and will be removed in next release. The config can only be set when the legacy query planner is explicitly enabled.", - "nullable": true, - "type": "boolean" - }, "generate_query_fragments": { "default": true, "description": "Enable QP generation of fragments for subgraph requests Default: true", @@ -8376,10 +8307,6 @@ expression: "&schema" "$ref": "#/definitions/Chaos", "description": "#/definitions/Chaos" }, - "experimental_query_planner_mode": { - "$ref": "#/definitions/QueryPlannerMode", - "description": "#/definitions/QueryPlannerMode" - }, "experimental_type_conditioned_fetching": { "default": false, "description": "Type conditioned fetching configuration.", diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@legacy_qp.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@legacy_qp.yaml.snap new file mode 100644 index 0000000000..44ea1678e6 --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@legacy_qp.yaml.snap @@ -0,0 +1,7 @@ +--- +source: apollo-router/src/configuration/tests.rs +expression: new_config +--- +--- +supergraph: + query_planning: {} diff --git a/apollo-router/src/configuration/testdata/metrics/query_planner_parallelism_auto.router.yaml b/apollo-router/src/configuration/testdata/metrics/query_planner_parallelism_auto.router.yaml deleted file mode 100644 index e29357f06d..0000000000 --- a/apollo-router/src/configuration/testdata/metrics/query_planner_parallelism_auto.router.yaml +++ /dev/null @@ -1,3 +0,0 @@ -supergraph: - query_planning: - experimental_parallelism: auto diff --git a/apollo-router/src/configuration/testdata/metrics/query_planner_parallelism_static.router.yaml b/apollo-router/src/configuration/testdata/metrics/query_planner_parallelism_static.router.yaml deleted file mode 100644 index 8861ab2777..0000000000 --- a/apollo-router/src/configuration/testdata/metrics/query_planner_parallelism_static.router.yaml +++ /dev/null @@ -1,3 +0,0 @@ -supergraph: - query_planning: - experimental_parallelism: 10 diff --git a/apollo-router/src/configuration/testdata/migrations/legacy_qp.yaml b/apollo-router/src/configuration/testdata/migrations/legacy_qp.yaml new file mode 100644 index 0000000000..4408d941ec --- /dev/null +++ b/apollo-router/src/configuration/testdata/migrations/legacy_qp.yaml @@ -0,0 +1,5 @@ +supergraph: + query_planning: + experimental_parallelism: 2 + experimental_reuse_query_fragments: true +experimental_query_planner_mode: legacy diff --git a/apollo-router/src/error.rs b/apollo-router/src/error.rs index 850634be2b..04efe566e9 100644 --- a/apollo-router/src/error.rs +++ b/apollo-router/src/error.rs @@ -7,15 +7,13 @@ use apollo_compiler::validation::WithErrors; use apollo_federation::error::FederationError; use displaydoc::Display; use lazy_static::__Deref; -use router_bridge::introspect::IntrospectionError; -use router_bridge::planner::PlannerError; -use router_bridge::planner::UsageReporting; use serde::Deserialize; use serde::Serialize; use thiserror::Error; use tokio::task::JoinError; use tower::BoxError; +use crate::apollo_studio_interop::UsageReporting; pub(crate) use crate::configuration::ConfigurationError; pub(crate) use crate::graphql::Error; use crate::graphql::ErrorExtension; @@ -236,9 +234,6 @@ impl From for CacheResolverError { /// Error types for service building. #[derive(Error, Debug, Display)] pub(crate) enum ServiceBuildError { - /// couldn't build Query Planner Service: {0} - QueryPlannerError(QueryPlannerError), - /// failed to initialize the query planner: {0} QpInitError(FederationError), @@ -255,18 +250,6 @@ impl From for ServiceBuildError { } } -impl From> for ServiceBuildError { - fn from(errors: Vec) -> Self { - ServiceBuildError::QueryPlannerError(errors.into()) - } -} - -impl From for ServiceBuildError { - fn from(error: router_bridge::error::Error) -> Self { - ServiceBuildError::QueryPlannerError(error.into()) - } -} - impl From for ServiceBuildError { fn from(err: BoxError) -> Self { ServiceBuildError::ServiceError(err) @@ -276,15 +259,9 @@ impl From for ServiceBuildError { /// Error types for QueryPlanner #[derive(Error, Debug, Display, Clone, Serialize, Deserialize)] pub(crate) enum QueryPlannerError { - /// couldn't instantiate query planner; invalid schema: {0} - SchemaValidationErrors(PlannerErrors), - /// invalid query: {0} OperationValidationErrors(ValidationErrors), - /// couldn't plan query: {0} - PlanningErrors(PlanErrors), - /// query planning panicked: {0} JoinError(String), @@ -297,15 +274,9 @@ pub(crate) enum QueryPlannerError { /// unhandled planner result UnhandledPlannerResult, - /// router bridge error: {0} - RouterBridgeError(router_bridge::error::Error), - /// spec error: {0} SpecError(SpecError), - /// introspection error: {0} - Introspection(IntrospectionError), - /// complexity limit exceeded LimitExceeded(OperationLimits), @@ -402,25 +373,11 @@ impl IntoGraphQLErrors for QueryPlannerError { QueryPlannerError::SpecError(err) => err .into_graphql_errors() .map_err(QueryPlannerError::SpecError), - QueryPlannerError::SchemaValidationErrors(errs) => errs - .into_graphql_errors() - .map_err(QueryPlannerError::SchemaValidationErrors), + QueryPlannerError::OperationValidationErrors(errs) => errs .into_graphql_errors() .map_err(QueryPlannerError::OperationValidationErrors), - QueryPlannerError::PlanningErrors(planning_errors) => Ok(planning_errors - .errors - .iter() - .map(|p_err| Error::from(p_err.clone())) - .collect()), - QueryPlannerError::Introspection(introspection_error) => Ok(vec![Error::builder() - .message( - introspection_error - .message - .unwrap_or_else(|| "introspection error".to_string()), - ) - .extension_code("INTROSPECTION_ERROR") - .build()]), + QueryPlannerError::LimitExceeded(OperationLimits { depth, height, @@ -471,7 +428,6 @@ impl IntoGraphQLErrors for QueryPlannerError { impl QueryPlannerError { pub(crate) fn usage_reporting(&self) -> Option { match self { - QueryPlannerError::PlanningErrors(pe) => Some(pe.usage_reporting.clone()), QueryPlannerError::SpecError(e) => Some(UsageReporting { stats_report_key: e.get_error_key().to_string(), referenced_fields_by_type: HashMap::new(), @@ -481,49 +437,6 @@ impl QueryPlannerError { } } -#[derive(Clone, Debug, Error, Serialize, Deserialize)] -/// Container for planner setup errors -pub(crate) struct PlannerErrors(Arc>); - -impl IntoGraphQLErrors for PlannerErrors { - fn into_graphql_errors(self) -> Result, Self> { - let errors = self.0.iter().map(|e| Error::from(e.clone())).collect(); - - Ok(errors) - } -} - -impl std::fmt::Display for PlannerErrors { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "schema validation errors: {}", - self.0 - .iter() - .map(std::string::ToString::to_string) - .collect::>() - .join(", ") - )) - } -} - -impl From> for QueryPlannerError { - fn from(errors: Vec) -> Self { - QueryPlannerError::SchemaValidationErrors(PlannerErrors(Arc::new(errors))) - } -} - -impl From for QueryPlannerError { - fn from(errors: router_bridge::planner::PlanErrors) -> Self { - QueryPlannerError::PlanningErrors(errors.into()) - } -} - -impl From for QueryPlannerError { - fn from(errors: PlanErrors) -> Self { - QueryPlannerError::PlanningErrors(errors) - } -} - impl From for QueryPlannerError { fn from(err: JoinError) -> Self { QueryPlannerError::JoinError(err.to_string()) @@ -552,12 +465,6 @@ impl From for QueryPlannerError { QueryPlannerError::OperationValidationErrors(ValidationErrors { errors: err.errors }) } } - -impl From for QueryPlannerError { - fn from(error: router_bridge::error::Error) -> Self { - QueryPlannerError::RouterBridgeError(error) - } -} impl From> for QueryPlannerError { fn from(error: OperationLimits) -> Self { QueryPlannerError::LimitExceeded(error) @@ -570,51 +477,6 @@ impl From for Response { } } -/// The payload if the plan_worker invocation failed -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct PlanErrors { - /// The errors the plan_worker invocation failed with - pub(crate) errors: Arc>, - /// Usage reporting related data such as the - /// operation signature and referenced fields - pub(crate) usage_reporting: UsageReporting, -} - -impl From for PlanErrors { - fn from( - router_bridge::planner::PlanErrors { - errors, - usage_reporting, - }: router_bridge::planner::PlanErrors, - ) -> Self { - PlanErrors { - errors, - usage_reporting, - } - } -} - -impl std::fmt::Display for PlanErrors { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "query validation errors: {}", - format_bridge_errors(&self.errors) - )) - } -} - -pub(crate) fn format_bridge_errors(errors: &[router_bridge::planner::PlanError]) -> String { - errors - .iter() - .map(|e| { - e.message - .clone() - .unwrap_or_else(|| "UNKNWON ERROR".to_string()) - }) - .collect::>() - .join(", ") -} - /// Error in the schema. #[derive(Debug, Error, Display, derive_more::From)] #[non_exhaustive] @@ -797,38 +659,4 @@ mod tests { assert_eq!(expected_gql_error, error.to_graphql_error(None)); } - - #[test] - fn test_into_graphql_error_introspection_with_message_handled_correctly() { - let expected_message = "no can introspect".to_string(); - let ie = IntrospectionError { - message: Some(expected_message.clone()), - }; - let error = QueryPlannerError::Introspection(ie); - let mut graphql_errors = error.into_graphql_errors().expect("vec of graphql errors"); - assert_eq!(graphql_errors.len(), 1); - let first_error = graphql_errors.pop().expect("has to be one error"); - assert_eq!(first_error.message, expected_message); - assert_eq!(first_error.extensions.len(), 1); - assert_eq!( - first_error.extensions.get("code").expect("has code"), - "INTROSPECTION_ERROR" - ); - } - - #[test] - fn test_into_graphql_error_introspection_without_message_handled_correctly() { - let expected_message = "introspection error".to_string(); - let ie = IntrospectionError { message: None }; - let error = QueryPlannerError::Introspection(ie); - let mut graphql_errors = error.into_graphql_errors().expect("vec of graphql errors"); - assert_eq!(graphql_errors.len(), 1); - let first_error = graphql_errors.pop().expect("has to be one error"); - assert_eq!(first_error.message, expected_message); - assert_eq!(first_error.extensions.len(), 1); - assert_eq!( - first_error.extensions.get("code").expect("has code"), - "INTROSPECTION_ERROR" - ); - } } diff --git a/apollo-router/src/graphql/mod.rs b/apollo-router/src/graphql/mod.rs index 7ec24d7dd7..0ac60d3f69 100644 --- a/apollo-router/src/graphql/mod.rs +++ b/apollo-router/src/graphql/mod.rs @@ -14,15 +14,8 @@ use heck::ToShoutySnakeCase; pub use request::Request; pub use response::IncrementalResponse; pub use response::Response; -pub use router_bridge::planner::Location; -use router_bridge::planner::PlanError; -use router_bridge::planner::PlanErrorExtensions; -use router_bridge::planner::PlannerError; -use router_bridge::planner::WorkerError; -use router_bridge::planner::WorkerGraphQLError; use serde::Deserialize; use serde::Serialize; -use serde_json_bytes::json; use serde_json_bytes::ByteString; use serde_json_bytes::Map as JsonMap; use serde_json_bytes::Value; @@ -44,6 +37,16 @@ pub use crate::json_ext::PathElement as JsonPathElement; /// even if that stream happens to only contain one item. pub type ResponseStream = Pin + Send>>; +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +/// The error location +pub struct Location { + /// The line number + pub line: u32, + /// The column number + pub column: u32, +} + /// A [GraphQL error](https://spec.graphql.org/October2021/#sec-Errors) /// as may be found in the `errors` field of a GraphQL [`Response`]. /// @@ -211,73 +214,6 @@ where } } -impl ErrorExtension for PlanError {} - -impl From for Error { - fn from(err: PlanError) -> Self { - let extension_code = err.extension_code(); - let extensions = err - .extensions - .map(convert_extensions_to_map) - .unwrap_or_else(move || { - let mut object = Object::new(); - object.insert("code", extension_code.into()); - object - }); - Self { - message: err.message.unwrap_or_else(|| String::from("plan error")), - extensions, - ..Default::default() - } - } -} - -impl ErrorExtension for PlannerError { - fn extension_code(&self) -> String { - match self { - PlannerError::WorkerGraphQLError(worker_graphql_error) => worker_graphql_error - .extensions - .as_ref() - .map(|ext| ext.code.clone()) - .unwrap_or_else(|| worker_graphql_error.extension_code()), - PlannerError::WorkerError(worker_error) => worker_error - .extensions - .as_ref() - .map(|ext| ext.code.clone()) - .unwrap_or_else(|| worker_error.extension_code()), - } - } -} - -impl From for Error { - fn from(err: PlannerError) -> Self { - match err { - PlannerError::WorkerGraphQLError(err) => err.into(), - PlannerError::WorkerError(err) => err.into(), - } - } -} - -impl ErrorExtension for WorkerError {} - -impl From for Error { - fn from(err: WorkerError) -> Self { - let extension_code = err.extension_code(); - let mut extensions = err - .extensions - .map(convert_extensions_to_map) - .unwrap_or_default(); - extensions.insert("code", extension_code.into()); - - Self { - message: err.message.unwrap_or_else(|| String::from("worker error")), - locations: err.locations.into_iter().map(Location::from).collect(), - extensions, - ..Default::default() - } - } -} - impl From for Error { fn from(error: CompilerExecutionError) -> Self { let CompilerExecutionError { @@ -315,37 +251,3 @@ impl From for Error { } } } - -impl ErrorExtension for WorkerGraphQLError {} - -impl From for Error { - fn from(err: WorkerGraphQLError) -> Self { - let extension_code = err.extension_code(); - let mut extensions = err - .extensions - .map(convert_extensions_to_map) - .unwrap_or_default(); - extensions.insert("code", extension_code.into()); - Self { - message: err.message, - locations: err.locations.into_iter().map(Location::from).collect(), - extensions, - ..Default::default() - } - } -} - -fn convert_extensions_to_map(ext: PlanErrorExtensions) -> Object { - let mut extensions = Object::new(); - extensions.insert("code", ext.code.into()); - if let Some(exception) = ext.exception { - extensions.insert( - "exception", - json!({ - "stacktrace": serde_json_bytes::Value::from(exception.stacktrace) - }), - ); - } - - extensions -} diff --git a/apollo-router/src/graphql/response.rs b/apollo-router/src/graphql/response.rs index 6b44f1d2cc..1f710e7add 100644 --- a/apollo-router/src/graphql/response.rs +++ b/apollo-router/src/graphql/response.rs @@ -276,11 +276,11 @@ impl From for Response { #[cfg(test)] mod tests { - use router_bridge::planner::Location; use serde_json::json; use serde_json_bytes::json as bjson; use super::*; + use crate::graphql::Location; #[test] fn test_append_errors_path_fallback_and_override() { diff --git a/apollo-router/src/lib.rs b/apollo-router/src/lib.rs index cc39b79aea..2a62b7c25b 100644 --- a/apollo-router/src/lib.rs +++ b/apollo-router/src/lib.rs @@ -110,16 +110,10 @@ pub mod _private { // Reexports for macros pub use linkme; pub use once_cell; - pub use router_bridge; pub use serde_json; pub use crate::plugin::PluginFactory; pub use crate::plugin::PLUGINS; - // For comparison/fuzzing - pub use crate::query_planner::bridge_query_planner::QueryPlanResult; - pub use crate::query_planner::plan_compare::diff_plan; - pub use crate::query_planner::plan_compare::plan_matches; - pub use crate::query_planner::plan_compare::render_diff; // For tests pub use crate::router_factory::create_test_service_factory_from_yaml; } 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 ca7217ba7f..d212501660 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 @@ -629,7 +629,6 @@ mod tests { use ahash::HashMapExt; use apollo_federation::query_plan::query_planner::QueryPlanner; use bytes::Bytes; - use router_bridge::planner::PlanOptions; use test_log::test; use tower::Service; @@ -638,6 +637,7 @@ mod tests { use crate::plugins::authorization::CacheKeyMetadata; use crate::query_planner::BridgeQueryPlanner; use crate::services::layers::query_analysis::ParsedDocument; + use crate::services::query_planner::PlanOptions; use crate::services::QueryPlannerContent; use crate::services::QueryPlannerRequest; use crate::spec; @@ -725,8 +725,6 @@ mod tests { let mut planner = BridgeQueryPlanner::new( schema.into(), config.clone(), - None, - None, Arc::new(IntrospectionCache::new(&config)), ) .await diff --git a/apollo-router/src/plugins/include_subgraph_errors.rs b/apollo-router/src/plugins/include_subgraph_errors.rs index 5641d0a506..5bc29a3237 100644 --- a/apollo-router/src/plugins/include_subgraph_errors.rs +++ b/apollo-router/src/plugins/include_subgraph_errors.rs @@ -88,7 +88,6 @@ impl Plugin for IncludeSubgraphErrors { #[cfg(test)] mod test { - use std::num::NonZeroUsize; use std::sync::Arc; use bytes::Bytes; @@ -211,14 +210,9 @@ mod test { include_str!("../../../apollo-router-benchmarks/benches/fixtures/supergraph.graphql"); let schema = Schema::parse(schema, &configuration).unwrap(); - let planner = BridgeQueryPlannerPool::new( - Vec::new(), - schema.into(), - Arc::clone(&configuration), - NonZeroUsize::new(1).unwrap(), - ) - .await - .unwrap(); + let planner = BridgeQueryPlannerPool::new(schema.into(), Arc::clone(&configuration)) + .await + .unwrap(); let schema = planner.schema(); let subgraph_schemas = planner.subgraph_schemas(); diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index 49fcbdf935..21f8993a93 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -47,7 +47,6 @@ use opentelemetry_semantic_conventions::trace::HTTP_REQUEST_METHOD; use parking_lot::Mutex; use parking_lot::RwLock; use rand::Rng; -use router_bridge::planner::UsageReporting; use serde_json_bytes::json; use serde_json_bytes::ByteString; use serde_json_bytes::Map; @@ -84,6 +83,7 @@ use self::tracing::apollo_telemetry::CLIENT_NAME_KEY; use self::tracing::apollo_telemetry::CLIENT_VERSION_KEY; use crate::apollo_studio_interop::ExtendedReferenceStats; use crate::apollo_studio_interop::ReferencedEnums; +use crate::apollo_studio_interop::UsageReporting; use crate::context::CONTAINS_GRAPHQL_ERROR; use crate::context::OPERATION_KIND; use crate::context::OPERATION_NAME; @@ -1899,7 +1899,7 @@ fn licensed_operation_count(stats_report_key: &str) -> u64 { } fn convert( - referenced_fields: router_bridge::planner::ReferencedFieldsForType, + referenced_fields: crate::apollo_studio_interop::ReferencedFieldsForType, ) -> crate::plugins::telemetry::apollo_exporter::proto::reports::ReferencedFieldsForType { crate::plugins::telemetry::apollo_exporter::proto::reports::ReferencedFieldsForType { field_names: referenced_fields.field_names, diff --git a/apollo-router/src/plugins/test.rs b/apollo-router/src/plugins/test.rs index 8535c9f5f1..0da442c3f2 100644 --- a/apollo-router/src/plugins/test.rs +++ b/apollo-router/src/plugins/test.rs @@ -15,7 +15,6 @@ use crate::plugin::DynPlugin; use crate::plugin::PluginInit; use crate::plugin::PluginPrivate; use crate::query_planner::BridgeQueryPlanner; -use crate::query_planner::PlannerMode; use crate::services::execution; use crate::services::http; use crate::services::router; @@ -95,17 +94,10 @@ impl> + 'static> PluginTestHarness { let schema = Schema::parse(schema, &config).unwrap(); let sdl = schema.raw_sdl.clone(); let supergraph = schema.supergraph_schema().clone(); - let rust_planner = PlannerMode::maybe_rust(&schema, &config).unwrap(); let introspection = Arc::new(IntrospectionCache::new(&config)); - let planner = BridgeQueryPlanner::new( - schema.into(), - Arc::new(config), - None, - rust_planner, - introspection, - ) - .await - .unwrap(); + let planner = BridgeQueryPlanner::new(schema.into(), Arc::new(config), introspection) + .await + .unwrap(); (sdl, supergraph, planner.subgraph_schemas()) } else { ( diff --git a/apollo-router/src/plugins/traffic_shaping/mod.rs b/apollo-router/src/plugins/traffic_shaping/mod.rs index 935a670164..3c803685cf 100644 --- a/apollo-router/src/plugins/traffic_shaping/mod.rs +++ b/apollo-router/src/plugins/traffic_shaping/mod.rs @@ -471,7 +471,6 @@ register_plugin!("apollo", "traffic_shaping", TrafficShaping); #[cfg(test)] mod test { - use std::num::NonZeroUsize; use std::sync::Arc; use bytes::Bytes; @@ -586,14 +585,9 @@ mod test { let config = Arc::new(config); let schema = Arc::new(Schema::parse(schema, &config).unwrap()); - let planner = BridgeQueryPlannerPool::new( - Vec::new(), - schema.clone(), - config.clone(), - NonZeroUsize::new(1).unwrap(), - ) - .await - .unwrap(); + let planner = BridgeQueryPlannerPool::new(schema.clone(), config.clone()) + .await + .unwrap(); let subgraph_schemas = planner.subgraph_schemas(); let mut builder = diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 10edd392ae..e14c174887 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -17,10 +17,6 @@ use futures::future::BoxFuture; use opentelemetry_api::metrics::MeterProvider as _; use opentelemetry_api::metrics::ObservableGauge; use opentelemetry_api::KeyValue; -use router_bridge::planner::PlanOptions; -use router_bridge::planner::PlanSuccess; -use router_bridge::planner::Planner; -use router_bridge::planner::UsageReporting; use serde::Deserialize; use serde_json_bytes::Value; use tower::Service; @@ -29,11 +25,8 @@ use super::PlanNode; use super::QueryKey; use crate::apollo_studio_interop::generate_usage_reporting; use crate::compute_job; -use crate::configuration::QueryPlannerMode; use crate::error::FederationErrorBridge; -use crate::error::PlanErrors; use crate::error::QueryPlannerError; -use crate::error::SchemaError; use crate::error::ServiceBuildError; use crate::error::ValidationErrors; use crate::graphql; @@ -47,11 +40,11 @@ use crate::plugins::authorization::UnauthorizedPaths; use crate::plugins::telemetry::config::ApolloSignatureNormalizationAlgorithm; use crate::plugins::telemetry::config::Conf as TelemetryConfig; use crate::query_planner::convert::convert_root_query_plan_node; -use crate::query_planner::dual_query_planner::BothModeComparisonJob; use crate::query_planner::fetch::QueryHash; use crate::query_planner::labeler::add_defer_labels; use crate::services::layers::query_analysis::ParsedDocument; use crate::services::layers::query_analysis::ParsedDocumentInner; +use crate::services::query_planner::PlanOptions; use crate::services::QueryPlannerContent; use crate::services::QueryPlannerRequest; use crate::services::QueryPlannerResponse; @@ -63,7 +56,6 @@ use crate::spec::SpecError; use crate::Configuration; pub(crate) const RUST_QP_MODE: &str = "rust"; -pub(crate) const JS_QP_MODE: &str = "js"; const UNSUPPORTED_FED1: &str = "fed1"; const INTERNAL_INIT_ERROR: &str = "internal"; @@ -82,13 +74,9 @@ pub(crate) struct BridgeQueryPlanner { introspection: Arc, } +// TODO: inline into parent struct #[derive(Clone)] pub(crate) enum PlannerMode { - Js(Arc>), - Both { - js: Arc>, - rust: Arc, - }, Rust(Arc), } @@ -109,67 +97,6 @@ fn federation_version_instrument(federation_version: Option) -> ObservableG } impl PlannerMode { - async fn new( - schema: &Schema, - configuration: &Configuration, - old_planner: &Option>>, - rust_planner: Option>, - ) -> Result { - Ok(match configuration.experimental_query_planner_mode { - QueryPlannerMode::New => Self::Rust( - rust_planner - .expect("expected Rust QP instance for `experimental_query_planner_mode: new`"), - ), - QueryPlannerMode::Legacy => { - Self::Js(Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?) - } - QueryPlannerMode::Both => Self::Both { - js: Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?, - rust: rust_planner.expect( - "expected Rust QP instance for `experimental_query_planner_mode: both`", - ), - }, - QueryPlannerMode::BothBestEffort => { - if let Some(rust) = rust_planner { - Self::Both { - js: Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?, - rust, - } - } else { - Self::Js(Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?) - } - } - QueryPlannerMode::NewBestEffort => { - if let Some(rust) = rust_planner { - Self::Rust(rust) - } else { - Self::Js(Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?) - } - } - }) - } - - pub(crate) fn maybe_rust( - schema: &Schema, - configuration: &Configuration, - ) -> Result>, ServiceBuildError> { - match configuration.experimental_query_planner_mode { - QueryPlannerMode::Legacy => Ok(None), - QueryPlannerMode::New | QueryPlannerMode::Both => { - Ok(Some(Self::rust(schema, configuration)?)) - } - QueryPlannerMode::BothBestEffort | QueryPlannerMode::NewBestEffort => { - match Self::rust(schema, configuration) { - Ok(planner) => Ok(Some(planner)), - Err(error) => { - tracing::info!("Falling back to the legacy query planner: {error}"); - Ok(None) - } - } - } - } - } - fn rust( schema: &Schema, configuration: &Configuration, @@ -197,55 +124,17 @@ impl PlannerMode { Ok(Arc::new(result.map_err(ServiceBuildError::QpInitError)?)) } - async fn js_planner( - sdl: &str, - configuration: &Configuration, - old_js_planner: &Option>>, - ) -> Result>, ServiceBuildError> { - let query_planner_configuration = configuration.js_query_planner_config(); - let planner = match old_js_planner { - None => Planner::new(sdl.to_owned(), query_planner_configuration).await?, - Some(old_planner) => { - old_planner - .update(sdl.to_owned(), query_planner_configuration) - .await? - } - }; - Ok(Arc::new(planner)) - } - async fn plan( &self, doc: &ParsedDocument, - filtered_query: String, operation: Option, plan_options: PlanOptions, // Initialization code that needs mutable access to the plan, // before we potentially share it in Arc with a background thread // for "both" mode. init_query_plan_root_node: impl Fn(&mut PlanNode) -> Result<(), ValidationErrors>, - ) -> Result, QueryPlannerError> { + ) -> Result { match self { - PlannerMode::Js(js) => { - let start = Instant::now(); - - let result = js.plan(filtered_query, operation, plan_options).await; - - let elapsed = start.elapsed().as_secs_f64(); - metric_query_planning_plan_duration(JS_QP_MODE, elapsed); - - let mut success = result - .map_err(QueryPlannerError::RouterBridgeError)? - .into_result() - .map_err(PlanErrors::from)?; - - if let Some(root_node) = &mut success.data.query_plan.node { - // Arc freshly deserialized from Deno should be unique, so this doesn’t clone: - let root_node = Arc::make_mut(root_node); - init_query_plan_root_node(root_node)?; - } - Ok(success) - } PlannerMode::Rust(rust_planner) => { let doc = doc.clone(); let rust_planner = rust_planner.clone(); @@ -294,95 +183,28 @@ impl PlannerMode { init_query_plan_root_node(node)?; } - // Dummy value overwritten below in `BrigeQueryPlanner::plan` - let usage_reporting = UsageReporting { - stats_report_key: Default::default(), - referenced_fields_by_type: Default::default(), - }; - - Ok(PlanSuccess { - usage_reporting, - data: QueryPlanResult { - formatted_query_plan: Some(Arc::new(plan.to_string())), - query_plan: QueryPlan { - node: root_node.map(Arc::new), - }, - evaluated_plan_count: plan - .statistics - .evaluated_plan_count - .clone() - .into_inner() as u64, + Ok(QueryPlanResult { + formatted_query_plan: Some(Arc::new(plan.to_string())), + query_plan: QueryPlan { + node: root_node.map(Arc::new), }, + evaluated_plan_count: plan.statistics.evaluated_plan_count.clone().into_inner() + as u64, }) } - PlannerMode::Both { js, rust } => { - let start = Instant::now(); - - let result = js - .plan(filtered_query, operation.clone(), plan_options.clone()) - .await; - - let elapsed = start.elapsed().as_secs_f64(); - metric_query_planning_plan_duration(JS_QP_MODE, elapsed); - - let mut js_result = result - .map_err(QueryPlannerError::RouterBridgeError)? - .into_result() - .map_err(PlanErrors::from); - - if let Ok(success) = &mut js_result { - if let Some(root_node) = &mut success.data.query_plan.node { - // Arc freshly deserialized from Deno should be unique, so this doesn’t clone: - let root_node = Arc::make_mut(root_node); - init_query_plan_root_node(root_node)?; - } - } - - let query_plan_options = QueryPlanOptions { - override_conditions: plan_options.override_conditions, - }; - BothModeComparisonJob { - rust_planner: rust.clone(), - js_duration: elapsed, - document: doc.executable.clone(), - operation_name: operation, - // Exclude usage reporting from the Result sent for comparison - js_result: js_result - .as_ref() - .map(|success| success.data.clone()) - .map_err(|e| e.errors.clone()), - plan_options: query_plan_options, - } - .schedule(); - - Ok(js_result?) - } } } async fn subgraphs( &self, ) -> Result>>, ServiceBuildError> { - let js = match self { - PlannerMode::Js(js) => js, - PlannerMode::Both { js, .. } => js, - PlannerMode::Rust(rust) => { - return Ok(rust - .subgraph_schemas() - .iter() - .map(|(name, schema)| (name.to_string(), Arc::new(schema.schema().clone()))) - .collect()) - } - }; - js.subgraphs() - .await? - .into_iter() - .map(|(name, schema_str)| { - let schema = apollo_compiler::Schema::parse_and_validate(schema_str, "") - .map_err(|errors| SchemaError::Validate(errors.into()))?; - Ok((name, Arc::new(schema))) - }) - .collect() + match self { + PlannerMode::Rust(rust) => Ok(rust + .subgraph_schemas() + .iter() + .map(|(name, schema)| (name.to_string(), Arc::new(schema.schema().clone()))) + .collect()), + } } } @@ -390,12 +212,9 @@ impl BridgeQueryPlanner { pub(crate) async fn new( schema: Arc, configuration: Arc, - old_js_planner: Option>>, - rust_planner: Option>, introspection_cache: Arc, ) -> Result { - let planner = - PlannerMode::new(&schema, &configuration, &old_js_planner, rust_planner).await?; + let planner = PlannerMode::Rust(PlannerMode::rust(&schema, &configuration)?); let subgraph_schemas = Arc::new(planner.subgraphs().await?); @@ -417,14 +236,6 @@ impl BridgeQueryPlanner { }) } - pub(crate) fn js_planner(&self) -> Option>> { - match &self.planner { - PlannerMode::Js(js) => Some(js.clone()), - PlannerMode::Both { js, .. } => Some(js.clone()), - PlannerMode::Rust(_) => None, - } - } - #[cfg(test)] pub(crate) fn schema(&self) -> Arc { self.schema.clone() @@ -489,90 +300,65 @@ impl BridgeQueryPlanner { doc: &ParsedDocument, query_metrics: OperationLimits, ) -> Result { - let plan_success = self + let plan_result = self .planner - .plan( - doc, - filtered_query.clone(), - operation.clone(), - plan_options, - |root_node| { - root_node.init_parsed_operations_and_hash_subqueries( - &self.subgraph_schemas, - &self.schema.raw_sdl, - )?; - root_node.extract_authorization_metadata(self.schema.supergraph_schema(), &key); - Ok(()) - }, - ) + .plan(doc, operation.clone(), plan_options, |root_node| { + root_node.init_parsed_operations_and_hash_subqueries( + &self.subgraph_schemas, + &self.schema.raw_sdl, + )?; + root_node.extract_authorization_metadata(self.schema.supergraph_schema(), &key); + Ok(()) + }) .await?; + let QueryPlanResult { + query_plan: QueryPlan { node }, + formatted_query_plan, + evaluated_plan_count, + } = plan_result; + + // If the query is filtered, we want to generate the signature using the original query and generate the + // reference using the filtered query. To do this, we need to re-parse the original query here. + let signature_doc = if original_query != filtered_query { + Query::parse_document( + &original_query, + operation.clone().as_deref(), + &self.schema, + &self.configuration, + ) + .unwrap_or(doc.clone()) + } else { + doc.clone() + }; - match plan_success { - PlanSuccess { - data: - QueryPlanResult { - query_plan: QueryPlan { node: Some(node) }, - formatted_query_plan, - evaluated_plan_count, - }, - mut usage_reporting, - } => { - // If the query is filtered, we want to generate the signature using the original query and generate the - // reference using the filtered query. To do this, we need to re-parse the original query here. - let signature_doc = if original_query != filtered_query { - Query::parse_document( - &original_query, - operation.clone().as_deref(), - &self.schema, - &self.configuration, - ) - .unwrap_or(doc.clone()) - } else { - doc.clone() - }; - - u64_histogram!( - "apollo.router.query_planning.plan.evaluated_plans", - "Number of query plans evaluated for a query before choosing the best one", - evaluated_plan_count - ); - - let generated_usage_reporting = generate_usage_reporting( - &signature_doc.executable, - &doc.executable, - &operation, - self.schema.supergraph_schema(), - &self.signature_normalization_algorithm, - ); - - usage_reporting.stats_report_key = - generated_usage_reporting.result.stats_report_key; - usage_reporting.referenced_fields_by_type = - generated_usage_reporting.result.referenced_fields_by_type; - - Ok(QueryPlannerContent::Plan { - plan: Arc::new(super::QueryPlan { - usage_reporting: Arc::new(usage_reporting), - root: node, - formatted_query_plan, - query: Arc::new(selections), - query_metrics, - estimated_size: Default::default(), - }), - }) - } - #[cfg_attr(feature = "failfast", allow(unused_variables))] - PlanSuccess { - data: - QueryPlanResult { - query_plan: QueryPlan { node: None }, - .. - }, - usage_reporting, - } => { - failfast_debug!("empty query plan"); - Err(QueryPlannerError::EmptyPlan(usage_reporting)) - } + let usage_reporting = generate_usage_reporting( + &signature_doc.executable, + &doc.executable, + &operation, + self.schema.supergraph_schema(), + &self.signature_normalization_algorithm, + ); + + if let Some(node) = node { + u64_histogram!( + "apollo.router.query_planning.plan.evaluated_plans", + "Number of query plans evaluated for a query before choosing the best one", + evaluated_plan_count + ); + + Ok(QueryPlannerContent::Plan { + plan: Arc::new(super::QueryPlan { + usage_reporting: Arc::new(usage_reporting), + root: node, + formatted_query_plan, + query: Arc::new(selections), + query_metrics, + estimated_size: Default::default(), + }), + }) + } else { + failfast_debug!("empty query plan"); + Err(QueryPlannerError::EmptyPlan(usage_reporting)) } } } @@ -782,18 +568,12 @@ impl BridgeQueryPlanner { // Note: Reexported under `apollo_router::_private` #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct QueryPlanResult { +pub(crate) struct QueryPlanResult { pub(super) formatted_query_plan: Option>, pub(super) query_plan: QueryPlan, pub(super) evaluated_plan_count: u64, } -impl QueryPlanResult { - pub fn formatted_query_plan(&self) -> Option<&str> { - self.formatted_query_plan.as_deref().map(String::as_str) - } -} - #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] /// The root query plan container. @@ -834,7 +614,6 @@ pub(crate) fn metric_rust_qp_init(init_error_kind: Option<&'static str>) { mod tests { use serde_json::json; use test_log::test; - use tower::Service; use tower::ServiceExt; use super::*; @@ -878,34 +657,24 @@ mod tests { #[test(tokio::test)] async fn federation_versions() { - async { - let sdl = include_str!("../testdata/minimal_fed1_supergraph.graphql"); - let config = Arc::default(); - let schema = Schema::parse(sdl, &config).unwrap(); - let introspection = Arc::new(IntrospectionCache::new(&config)); - let _planner = - BridgeQueryPlanner::new(schema.into(), config, None, None, introspection) - .await - .unwrap(); - - assert_gauge!( - "apollo.router.supergraph.federation", - 1, - federation.version = 1 - ); - } - .with_metrics() - .await; + let sdl = include_str!("../testdata/minimal_fed1_supergraph.graphql"); + let config = Arc::default(); + let schema = Schema::parse(sdl, &config).unwrap(); + let introspection = Arc::new(IntrospectionCache::new(&config)); + let error = BridgeQueryPlanner::new(schema.into(), config, introspection) + .await + .err() + .expect("expected error for fed1 supergraph"); + assert_eq!(error.to_string(), "failed to initialize the query planner: Supergraphs composed with federation version 1 are not supported. Please recompose your supergraph with federation version 2 or greater"); async { let sdl = include_str!("../testdata/minimal_supergraph.graphql"); let config = Arc::default(); let schema = Schema::parse(sdl, &config).unwrap(); let introspection = Arc::new(IntrospectionCache::new(&config)); - let _planner = - BridgeQueryPlanner::new(schema.into(), config, None, None, introspection) - .await - .unwrap(); + let _planner = BridgeQueryPlanner::new(schema.into(), config, introspection) + .await + .unwrap(); assert_gauge!( "apollo.router.supergraph.federation", @@ -926,8 +695,6 @@ mod tests { let planner = BridgeQueryPlanner::new( schema.clone(), Default::default(), - None, - None, Arc::new(IntrospectionCache::new(&config)), ) .await @@ -1033,8 +800,6 @@ mod tests { let planner = BridgeQueryPlanner::new( schema.into(), configuration.clone(), - None, - None, Arc::new(IntrospectionCache::new(&configuration)), ) .await @@ -1347,8 +1112,6 @@ mod tests { let planner = BridgeQueryPlanner::new( schema.into(), configuration.clone(), - None, - None, Arc::new(IntrospectionCache::new(&configuration)), ) .await @@ -1375,30 +1138,6 @@ mod tests { .await } - #[tokio::test] - async fn test_both_mode() { - let mut harness = crate::TestHarness::builder() - // auth is not relevant here, but supergraph.graphql uses join/v0.1 - // which is not supported by the Rust query planner - .schema(include_str!("../../tests/fixtures/supergraph-auth.graphql")) - .configuration_json(serde_json::json!({ - "experimental_query_planner_mode": "both", - })) - .unwrap() - .build_supergraph() - .await - .unwrap(); - - let request = supergraph::Request::fake_builder() - .query("{ topProducts { name }}") - .build() - .unwrap(); - let mut response = harness.ready().await.unwrap().call(request).await.unwrap(); - assert!(response.response.status().is_success()); - let response = response.next_response().await.unwrap(); - assert!(response.errors.is_empty()); - } - #[tokio::test] async fn test_rust_mode_subgraph_operation_serialization() { let subgraph_queries = Arc::new(tokio::sync::Mutex::new(String::new())); @@ -1407,10 +1146,6 @@ mod tests { // auth is not relevant here, but supergraph.graphql uses join/v0.1 // which is not supported by the Rust query planner .schema(include_str!("../../tests/fixtures/supergraph-auth.graphql")) - .configuration_json(serde_json::json!({ - "experimental_query_planner_mode": "new", - })) - .unwrap() .subgraph_hook(move |_name, _default| { let subgraph_queries = Arc::clone(&subgraph_queries); tower::service_fn(move |request: subgraph::Request| { @@ -1462,15 +1197,6 @@ mod tests { f64, "planner" = "rust" ); - - let start = Instant::now(); - let elapsed = start.elapsed().as_secs_f64(); - metric_query_planning_plan_duration(JS_QP_MODE, elapsed); - assert_histogram_exists!( - "apollo.router.query_planning.plan.duration", - f64, - "planner" = "js" - ); } #[test] 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 5246b79901..ac0e554b06 100644 --- a/apollo-router/src/query_planner/bridge_query_planner_pool.rs +++ b/apollo-router/src/query_planner/bridge_query_planner_pool.rs @@ -1,249 +1,59 @@ use std::collections::HashMap; -use std::num::NonZeroUsize; -use std::sync::atomic::AtomicU64; -use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::Mutex; use std::task::Poll; use std::time::Instant; use apollo_compiler::validation::Valid; -use async_channel::bounded; -use async_channel::Sender; use futures::future::BoxFuture; -use opentelemetry::metrics::MeterProvider; use opentelemetry::metrics::ObservableGauge; -use opentelemetry::metrics::Unit; -use router_bridge::planner::Planner; -use tokio::sync::oneshot; -use tokio::task::JoinSet; -use tower::Service; -use tower::ServiceExt; use super::bridge_query_planner::BridgeQueryPlanner; -use super::QueryPlanResult; -use crate::configuration::QueryPlannerMode; use crate::error::QueryPlannerError; use crate::error::ServiceBuildError; use crate::introspection::IntrospectionCache; -use crate::metrics::meter_provider; -use crate::query_planner::PlannerMode; use crate::services::QueryPlannerRequest; use crate::services::QueryPlannerResponse; use crate::spec::Schema; use crate::Configuration; -static CHANNEL_SIZE: usize = 1_000; - #[derive(Clone)] pub(crate) struct BridgeQueryPlannerPool { - js_planners: Vec>>, pool_mode: PoolMode, schema: Arc, subgraph_schemas: Arc>>>, compute_jobs_queue_size_gauge: Arc>>>, - v8_heap_used: Arc, - v8_heap_used_gauge: Arc>>>, - v8_heap_total: Arc, - v8_heap_total_gauge: Arc>>>, introspection_cache: Arc, } +// TODO: remove #[derive(Clone)] enum PoolMode { - Pool { - sender: Sender<( - QueryPlannerRequest, - oneshot::Sender>, - )>, - pool_size_gauge: Arc>>>, - }, - PassThrough { - delegate: BridgeQueryPlanner, - }, + PassThrough { delegate: BridgeQueryPlanner }, } impl BridgeQueryPlannerPool { pub(crate) async fn new( - old_js_planners: Vec>>, schema: Arc, configuration: Arc, - size: NonZeroUsize, ) -> Result { - let rust_planner = PlannerMode::maybe_rust(&schema, &configuration)?; - - let mut old_js_planners_iterator = old_js_planners.into_iter(); - // All query planners in the pool now share the same introspection cache. // This allows meaningful gauges, and it makes sense that queries should be cached across all planners. let introspection_cache = Arc::new(IntrospectionCache::new(&configuration)); - let pool_mode; - let js_planners; - let subgraph_schemas; - if let QueryPlannerMode::New = configuration.experimental_query_planner_mode { - let old_planner = old_js_planners_iterator.next(); - let delegate = BridgeQueryPlanner::new( - schema.clone(), - configuration, - old_planner, - rust_planner, - introspection_cache.clone(), - ) - .await?; - js_planners = delegate.js_planner().into_iter().collect::>(); - subgraph_schemas = delegate.subgraph_schemas(); - pool_mode = PoolMode::PassThrough { delegate } - } else { - let mut join_set = JoinSet::new(); - let (sender, receiver) = bounded::<( - QueryPlannerRequest, - oneshot::Sender>, - )>(CHANNEL_SIZE); - - for _ in 0..size.into() { - let schema = schema.clone(); - let configuration = configuration.clone(); - let rust_planner = rust_planner.clone(); - let introspection_cache = introspection_cache.clone(); - - let old_planner = old_js_planners_iterator.next(); - join_set.spawn(async move { - BridgeQueryPlanner::new( - schema, - configuration, - old_planner, - rust_planner, - introspection_cache, - ) - .await - }); - } - - let mut bridge_query_planners = Vec::new(); - - while let Some(task_result) = join_set.join_next().await { - let bridge_query_planner = - task_result.map_err(|e| ServiceBuildError::ServiceError(Box::new(e)))??; - bridge_query_planners.push(bridge_query_planner); - } - - subgraph_schemas = bridge_query_planners - .first() - .ok_or_else(|| { - ServiceBuildError::QueryPlannerError(QueryPlannerError::PoolProcessing( - "There should be at least 1 Query Planner service in pool".to_string(), - )) - })? - .subgraph_schemas(); - - js_planners = bridge_query_planners - .iter() - .filter_map(|p| p.js_planner()) - .collect(); - - for mut planner in bridge_query_planners.into_iter() { - let receiver = receiver.clone(); - - tokio::spawn(async move { - while let Ok((request, res_sender)) = receiver.recv().await { - let svc = match planner.ready().await { - Ok(svc) => svc, - Err(e) => { - let _ = res_sender.send(Err(e)); - - continue; - } - }; - - let res = svc.call(request).await; - - let _ = res_sender.send(res); - } - }); - } - pool_mode = PoolMode::Pool { - sender, - pool_size_gauge: Default::default(), - } - } - let v8_heap_used: Arc = Default::default(); - let v8_heap_total: Arc = Default::default(); - - // initialize v8 metrics - if let Some(bridge_query_planner) = js_planners.first().cloned() { - Self::get_v8_metrics( - bridge_query_planner, - v8_heap_used.clone(), - v8_heap_total.clone(), - ) - .await; - } + let delegate = + BridgeQueryPlanner::new(schema.clone(), configuration, introspection_cache.clone()) + .await?; Ok(Self { - js_planners, - pool_mode, + subgraph_schemas: delegate.subgraph_schemas(), + pool_mode: PoolMode::PassThrough { delegate }, schema, - subgraph_schemas, compute_jobs_queue_size_gauge: Default::default(), - v8_heap_used, - v8_heap_used_gauge: Default::default(), - v8_heap_total, - v8_heap_total_gauge: Default::default(), introspection_cache, }) } - fn create_pool_size_gauge(&self) { - if let PoolMode::Pool { - sender, - pool_size_gauge, - } = &self.pool_mode - { - let sender = sender.clone(); - let meter = meter_provider().meter("apollo/router"); - let gauge = meter - .u64_observable_gauge("apollo.router.query_planning.queued") - .with_description("Number of queries waiting to be planned") - .with_unit(Unit::new("query")) - .with_callback(move |m| m.observe(sender.len() as u64, &[])) - .init(); - *pool_size_gauge.lock().expect("lock poisoned") = Some(gauge); - } - } - - fn create_heap_used_gauge(&self) -> ObservableGauge { - let meter = meter_provider().meter("apollo/router"); - let current_heap_used_for_gauge = self.v8_heap_used.clone(); - let heap_used_gauge = meter - .u64_observable_gauge("apollo.router.v8.heap.used") - .with_description("V8 heap used, in bytes") - .with_unit(Unit::new("By")) - .with_callback(move |i| { - i.observe(current_heap_used_for_gauge.load(Ordering::SeqCst), &[]) - }) - .init(); - heap_used_gauge - } - - fn create_heap_total_gauge(&self) -> ObservableGauge { - let meter = meter_provider().meter("apollo/router"); - let current_heap_total_for_gauge = self.v8_heap_total.clone(); - let heap_total_gauge = meter - .u64_observable_gauge("apollo.router.v8.heap.total") - .with_description("V8 heap total, in bytes") - .with_unit(Unit::new("By")) - .with_callback(move |i| { - i.observe(current_heap_total_for_gauge.load(Ordering::SeqCst), &[]) - }) - .init(); - heap_total_gauge - } - - pub(crate) fn js_planners(&self) -> Vec>> { - self.js_planners.clone() - } - pub(crate) fn schema(&self) -> Arc { self.schema.clone() } @@ -254,18 +64,6 @@ impl BridgeQueryPlannerPool { self.subgraph_schemas.clone() } - async fn get_v8_metrics( - planner: Arc>, - v8_heap_used: Arc, - v8_heap_total: Arc, - ) { - let metrics = planner.get_heap_statistics().await; - if let Ok(metrics) = metrics { - v8_heap_used.store(metrics.heap_used, Ordering::SeqCst); - v8_heap_total.store(metrics.heap_total, Ordering::SeqCst); - } - } - pub(super) fn activate(&self) { // Gauges MUST be initialized after a meter provider is created. // When a hot reload happens this means that the gauges must be re-initialized. @@ -273,11 +71,6 @@ impl BridgeQueryPlannerPool { .compute_jobs_queue_size_gauge .lock() .expect("lock poisoned") = Some(crate::compute_job::create_queue_size_gauge()); - self.create_pool_size_gauge(); - *self.v8_heap_used_gauge.lock().expect("lock poisoned") = - Some(self.create_heap_used_gauge()); - *self.v8_heap_total_gauge.lock().expect("lock poisoned") = - Some(self.create_heap_total_gauge()); self.introspection_cache.activate(); } } @@ -291,42 +84,17 @@ impl tower::Service for BridgeQueryPlannerPool { fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> Poll> { if crate::compute_job::is_full() { - return Poll::Pending; - } - match &self.pool_mode { - PoolMode::Pool { sender, .. } if sender.is_full() => Poll::Ready(Err( - QueryPlannerError::PoolProcessing("query plan queue is full".into()), - )), - _ => Poll::Ready(Ok(())), + Poll::Pending + } else { + Poll::Ready(Ok(())) } } fn call(&mut self, req: QueryPlannerRequest) -> Self::Future { let pool_mode = self.pool_mode.clone(); - - let get_metrics_future = - if let Some(bridge_query_planner) = self.js_planners.first().cloned() { - Some(Self::get_v8_metrics( - bridge_query_planner, - self.v8_heap_used.clone(), - self.v8_heap_total.clone(), - )) - } else { - None - }; - Box::pin(async move { let start; let res = match pool_mode { - PoolMode::Pool { sender, .. } => { - let (response_sender, response_receiver) = oneshot::channel(); - start = Instant::now(); - let _ = sender.send((req, response_sender)).await; - - response_receiver - .await - .map_err(|_| QueryPlannerError::UnhandledPlannerResult)? - } PoolMode::PassThrough { mut delegate } => { start = Instant::now(); delegate.call(req).await @@ -339,85 +107,7 @@ impl tower::Service for BridgeQueryPlannerPool { start.elapsed().as_secs_f64() ); - if let Some(f) = get_metrics_future { - // execute in a separate task to avoid blocking the request - tokio::task::spawn(f); - } - res }) } } - -#[cfg(test)] - -mod tests { - use opentelemetry_sdk::metrics::data::Gauge; - use router_bridge::planner::PlanOptions; - - use super::*; - use crate::metrics::FutureMetricsExt; - use crate::plugins::authorization::CacheKeyMetadata; - use crate::spec::Query; - use crate::Context; - - #[tokio::test] - async fn test_v8_metrics() { - let sdl = include_str!("../testdata/supergraph.graphql"); - let config = Arc::default(); - let schema = Arc::new(Schema::parse(sdl, &config).unwrap()); - - async move { - let mut pool = BridgeQueryPlannerPool::new( - Vec::new(), - schema.clone(), - config.clone(), - NonZeroUsize::new(2).unwrap(), - ) - .await - .unwrap(); - pool.activate(); - let query = "query { me { name } }".to_string(); - - let doc = Query::parse_document(&query, None, &schema, &config).unwrap(); - let context = Context::new(); - context - .extensions() - .with_lock(|mut lock| lock.insert(doc.clone())); - - pool.call(QueryPlannerRequest::new( - query, - None, - doc, - CacheKeyMetadata::default(), - PlanOptions::default(), - )) - .await - .unwrap(); - - let metrics = crate::metrics::collect_metrics(); - let heap_used = metrics.find("apollo.router.v8.heap.used").unwrap(); - let heap_total = metrics.find("apollo.router.v8.heap.total").unwrap(); - - println!( - "got heap_used: {:?}, heap_total: {:?}", - heap_used - .data - .as_any() - .downcast_ref::>() - .unwrap() - .data_points[0] - .value, - heap_total - .data - .as_any() - .downcast_ref::>() - .unwrap() - .data_points[0] - .value - ); - } - .with_metrics() - .await; - } -} diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index 7b1f8c5279..e5ea4941e4 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -10,10 +10,6 @@ use indexmap::IndexMap; use query_planner::QueryPlannerPlugin; use rand::seq::SliceRandom; use rand::thread_rng; -use router_bridge::planner::PlanOptions; -use router_bridge::planner::Planner; -use router_bridge::planner::QueryPlannerConfig; -use router_bridge::planner::UsageReporting; use sha2::Digest; use sha2::Sha256; use tower::BoxError; @@ -23,6 +19,7 @@ use tower_service::Service; use tracing::Instrument; use super::fetch::QueryHash; +use crate::apollo_studio_interop::UsageReporting; use crate::cache::estimate_size; use crate::cache::storage::InMemoryCache; use crate::cache::storage::ValueType; @@ -36,11 +33,11 @@ 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::BridgeQueryPlannerPool; -use crate::query_planner::QueryPlanResult; use crate::services::layers::persisted_queries::PersistedQueryLayer; use crate::services::layers::query_analysis::ParsedDocument; use crate::services::layers::query_analysis::QueryAnalysisLayer; use crate::services::query_planner; +use crate::services::query_planner::PlanOptions; use crate::services::QueryPlannerContent; use crate::services::QueryPlannerRequest; use crate::services::QueryPlannerResponse; @@ -54,14 +51,6 @@ pub(crate) type InMemoryCachePlanner = InMemoryCache>>; pub(crate) const APOLLO_OPERATION_ID: &str = "apollo_operation_id"; -#[derive(Debug, Clone, Hash)] -pub(crate) enum ConfigMode { - Rust(Arc), - Both(Arc), - BothBestEffort(Arc), - Js(Arc), -} - /// A query planner wrapper that caches results. /// /// The query planner performs LRU caching. @@ -121,37 +110,7 @@ where AuthorizationPlugin::enable_directives(configuration, &schema).unwrap_or(false); let mut hasher = StructHasher::new(); - match configuration.experimental_query_planner_mode { - crate::configuration::QueryPlannerMode::New => { - "PLANNER-NEW".hash(&mut hasher); - ConfigMode::Rust(Arc::new(configuration.rust_query_planner_config())) - .hash(&mut hasher); - } - crate::configuration::QueryPlannerMode::Legacy => { - "PLANNER-LEGACY".hash(&mut hasher); - ConfigMode::Js(Arc::new(configuration.js_query_planner_config())).hash(&mut hasher); - } - crate::configuration::QueryPlannerMode::Both => { - "PLANNER-BOTH".hash(&mut hasher); - ConfigMode::Both(Arc::new(configuration.js_query_planner_config())) - .hash(&mut hasher); - ConfigMode::Rust(Arc::new(configuration.rust_query_planner_config())) - .hash(&mut hasher); - } - crate::configuration::QueryPlannerMode::BothBestEffort => { - "PLANNER-BOTH-BEST-EFFORT".hash(&mut hasher); - ConfigMode::BothBestEffort(Arc::new(configuration.js_query_planner_config())) - .hash(&mut hasher); - ConfigMode::Rust(Arc::new(configuration.rust_query_planner_config())) - .hash(&mut hasher); - } - crate::configuration::QueryPlannerMode::NewBestEffort => { - "PLANNER-NEW-BEST-EFFORT".hash(&mut hasher); - ConfigMode::Js(Arc::new(configuration.js_query_planner_config())).hash(&mut hasher); - ConfigMode::Rust(Arc::new(configuration.rust_query_planner_config())) - .hash(&mut hasher); - } - }; + configuration.rust_query_planner_config().hash(&mut hasher); let config_mode_hash = Arc::new(QueryHash(hasher.finalize())); Ok(Self { @@ -378,10 +337,6 @@ where } impl CachingQueryPlanner { - pub(crate) fn js_planners(&self) -> Vec>> { - self.delegate.js_planners() - } - pub(crate) fn subgraph_schemas( &self, ) -> Arc>>> { @@ -717,12 +672,11 @@ impl ValueType for Result> { mod tests { use mockall::mock; use mockall::predicate::*; - use router_bridge::planner::UsageReporting; use test_log::test; use tower::Service; use super::*; - use crate::error::PlanErrors; + use crate::apollo_studio_interop::UsageReporting; use crate::json_ext::Object; use crate::query_planner::QueryPlan; use crate::spec::Query; @@ -769,15 +723,10 @@ mod tests { let mut delegate = MockMyQueryPlanner::new(); delegate.expect_clone().returning(|| { let mut planner = MockMyQueryPlanner::new(); - planner.expect_sync_call().times(0..2).returning(|_| { - Err(QueryPlannerError::from(PlanErrors { - errors: Default::default(), - usage_reporting: UsageReporting { - stats_report_key: "this is a test key".to_string(), - referenced_fields_by_type: Default::default(), - }, - })) - }); + planner + .expect_sync_call() + .times(0..2) + .returning(|_| Err(QueryPlannerError::UnhandledPlannerResult)); planner }); diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs deleted file mode 100644 index 3996d4f22c..0000000000 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ /dev/null @@ -1,182 +0,0 @@ -//! Running two query planner implementations and comparing their results - -use std::sync::Arc; -use std::sync::OnceLock; -use std::time::Instant; - -use apollo_compiler::validation::Valid; -use apollo_compiler::ExecutableDocument; -use apollo_compiler::Name; -use apollo_federation::error::FederationError; -use apollo_federation::query_plan::query_planner::QueryPlanOptions; -use apollo_federation::query_plan::query_planner::QueryPlanner; - -use crate::error::format_bridge_errors; -use crate::query_planner::bridge_query_planner::metric_query_planning_plan_duration; -use crate::query_planner::bridge_query_planner::JS_QP_MODE; -use crate::query_planner::bridge_query_planner::RUST_QP_MODE; -use crate::query_planner::convert::convert_root_query_plan_node; -use crate::query_planner::plan_compare::diff_plan; -use crate::query_planner::plan_compare::opt_plan_node_matches; -use crate::query_planner::QueryPlanResult; - -/// Jobs are dropped if this many are already queued -const QUEUE_SIZE: usize = 10; -const WORKER_THREAD_COUNT: usize = 1; - -pub(crate) struct BothModeComparisonJob { - pub(crate) rust_planner: Arc, - pub(crate) js_duration: f64, - pub(crate) document: Arc>, - pub(crate) operation_name: Option, - pub(crate) js_result: Result>>, - pub(crate) plan_options: QueryPlanOptions, -} - -type Queue = crossbeam_channel::Sender; - -static QUEUE: OnceLock = OnceLock::new(); - -fn queue() -> &'static Queue { - QUEUE.get_or_init(|| { - let (sender, receiver) = crossbeam_channel::bounded::(QUEUE_SIZE); - for _ in 0..WORKER_THREAD_COUNT { - let job_receiver = receiver.clone(); - std::thread::spawn(move || { - for job in job_receiver { - job.execute() - } - }); - } - sender - }) -} - -impl BothModeComparisonJob { - pub(crate) fn schedule(self) { - // We use a bounded queue: try_send returns an error when full. This is fine. - // We prefer dropping some comparison jobs and only gathering some of the data - // rather than consume too much resources. - // - // Either way we move on and let this thread continue proceed with the query plan from JS. - let _ = queue().try_send(self).is_err(); - } - - fn execute(self) { - let start = Instant::now(); - - let rust_result = self - .operation_name - .as_deref() - .map(|n| Name::new(n).map_err(FederationError::from)) - .transpose() - .and_then(|operation| { - self.rust_planner - .build_query_plan(&self.document, operation, self.plan_options) - }); - - let elapsed = start.elapsed().as_secs_f64(); - metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); - - metric_query_planning_plan_both_comparison_duration(RUST_QP_MODE, elapsed); - metric_query_planning_plan_both_comparison_duration(JS_QP_MODE, self.js_duration); - - let name = self.operation_name.as_deref(); - let operation_desc = if let Ok(operation) = self.document.operations.get(name) { - if let Some(parsed_name) = &operation.name { - format!(" in {} `{parsed_name}`", operation.operation_type) - } else { - format!(" in anonymous {}", operation.operation_type) - } - } else { - String::new() - }; - - let is_matched; - match (&self.js_result, &rust_result) { - (Err(js_errors), Ok(_)) => { - tracing::warn!( - "JS query planner error{operation_desc}: {}", - format_bridge_errors(js_errors) - ); - is_matched = false; - } - (Ok(_), Err(rust_error)) => { - tracing::warn!("Rust query planner error{operation_desc}: {}", rust_error); - is_matched = false; - } - (Err(_), Err(_)) => { - is_matched = true; - } - - (Ok(js_plan), Ok(rust_plan)) => { - let js_root_node = &js_plan.query_plan.node; - let rust_root_node = convert_root_query_plan_node(rust_plan); - let match_result = opt_plan_node_matches(js_root_node, &rust_root_node); - is_matched = match_result.is_ok(); - match match_result { - Ok(_) => tracing::trace!("JS and Rust query plans match{operation_desc}! 🎉"), - Err(err) => { - tracing::debug!("JS v.s. Rust query plan mismatch{operation_desc}"); - tracing::debug!("{}", err.full_description()); - tracing::debug!( - "Diff of formatted plans:\n{}", - diff_plan(js_plan, rust_plan) - ); - tracing::trace!("JS query plan Debug: {js_root_node:#?}"); - tracing::trace!("Rust query plan Debug: {rust_root_node:#?}"); - } - } - } - } - - u64_counter!( - "apollo.router.operations.query_planner.both", - "Comparing JS v.s. Rust query plans", - 1, - "generation.is_matched" = is_matched, - "generation.js_error" = self.js_result.is_err(), - "generation.rust_error" = rust_result.is_err() - ); - } -} - -pub(crate) fn metric_query_planning_plan_both_comparison_duration( - planner: &'static str, - elapsed: f64, -) { - f64_histogram!( - "apollo.router.operations.query_planner.both.duration", - "Comparing JS v.s. Rust query plan duration.", - elapsed, - "planner" = planner - ); -} - -#[cfg(test)] -mod tests { - use std::time::Instant; - - use super::*; - - #[test] - fn test_metric_query_planning_plan_both_comparison_duration() { - let start = Instant::now(); - let elapsed = start.elapsed().as_secs_f64(); - metric_query_planning_plan_both_comparison_duration(RUST_QP_MODE, elapsed); - assert_histogram_exists!( - "apollo.router.operations.query_planner.both.duration", - f64, - "planner" = "rust" - ); - - let start = Instant::now(); - let elapsed = start.elapsed().as_secs_f64(); - metric_query_planning_plan_both_comparison_duration(JS_QP_MODE, elapsed); - assert_histogram_exists!( - "apollo.router.operations.query_planner.both.duration", - f64, - "planner" = "js" - ); - } -} diff --git a/apollo-router/src/query_planner/mod.rs b/apollo-router/src/query_planner/mod.rs index fb912e1d16..d26237c712 100644 --- a/apollo-router/src/query_planner/mod.rs +++ b/apollo-router/src/query_planner/mod.rs @@ -14,12 +14,10 @@ pub(crate) mod bridge_query_planner; mod bridge_query_planner_pool; mod caching_query_planner; mod convert; -pub(crate) mod dual_query_planner; mod execution; pub(crate) mod fetch; mod labeler; mod plan; -pub(crate) mod plan_compare; pub(crate) mod rewrites; mod selection; mod subgraph_context; diff --git a/apollo-router/src/query_planner/plan.rs b/apollo-router/src/query_planner/plan.rs index f0e5763358..076c47080c 100644 --- a/apollo-router/src/query_planner/plan.rs +++ b/apollo-router/src/query_planner/plan.rs @@ -3,14 +3,13 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use apollo_compiler::validation::Valid; -use router_bridge::planner::PlanOptions; -use router_bridge::planner::UsageReporting; use serde::Deserialize; use serde::Serialize; pub(crate) use self::fetch::OperationKind; use super::fetch; use super::subscription::SubscriptionNode; +use crate::apollo_studio_interop::UsageReporting; use crate::cache::estimate_size; use crate::configuration::Batching; use crate::error::CacheResolverError; @@ -21,6 +20,7 @@ use crate::json_ext::Value; use crate::plugins::authorization::CacheKeyMetadata; use crate::query_planner::fetch::QueryHash; use crate::query_planner::fetch::SubgraphSchemas; +use crate::services::query_planner::PlanOptions; use crate::spec::operation_limits::OperationLimits; use crate::spec::Query; diff --git a/apollo-router/src/query_planner/plan_compare.rs b/apollo-router/src/query_planner/plan_compare.rs deleted file mode 100644 index 7367ac2caa..0000000000 --- a/apollo-router/src/query_planner/plan_compare.rs +++ /dev/null @@ -1,1303 +0,0 @@ -// Semantic comparison of JS and Rust query plans - -use std::borrow::Borrow; -use std::collections::hash_map::HashMap; -use std::fmt::Write; -use std::hash::DefaultHasher; -use std::hash::Hash; -use std::hash::Hasher; - -use apollo_compiler::ast; -use apollo_compiler::Name; -use apollo_compiler::Node; -use apollo_federation::query_plan::QueryPlan as NativeQueryPlan; - -use super::convert::convert_root_query_plan_node; -use super::fetch::FetchNode; -use super::fetch::SubgraphOperation; -use super::rewrites::DataRewrite; -use super::selection::Selection; -use super::subscription::SubscriptionNode; -use super::DeferredNode; -use super::FlattenNode; -use super::PlanNode; -use super::Primary; -use super::QueryPlanResult; -use crate::json_ext::Path; -use crate::json_ext::PathElement; - -//================================================================================================== -// Public interface - -pub struct MatchFailure { - description: String, - backtrace: std::backtrace::Backtrace, -} - -impl MatchFailure { - pub fn description(&self) -> String { - self.description.clone() - } - - pub fn full_description(&self) -> String { - format!("{}\n\nBacktrace:\n{}", self.description, self.backtrace) - } - - fn new(description: String) -> MatchFailure { - MatchFailure { - description, - backtrace: std::backtrace::Backtrace::force_capture(), - } - } - - fn add_description(self: MatchFailure, description: &str) -> MatchFailure { - MatchFailure { - description: format!("{}\n{}", self.description, description), - backtrace: self.backtrace, - } - } -} - -macro_rules! check_match { - ($pred:expr) => { - if !$pred { - return Err(MatchFailure::new(format!( - "mismatch at {}", - stringify!($pred) - ))); - } - }; -} - -macro_rules! check_match_eq { - ($a:expr, $b:expr) => { - if $a != $b { - let message = format!( - "mismatch between {} and {}:\nleft: {:?}\nright: {:?}", - stringify!($a), - stringify!($b), - $a, - $b - ); - return Err(MatchFailure::new(message)); - } - }; -} - -// Note: Reexported under `apollo_router::_private` -pub fn plan_matches( - js_plan: &QueryPlanResult, - rust_plan: &NativeQueryPlan, -) -> Result<(), MatchFailure> { - let js_root_node = &js_plan.query_plan.node; - let rust_root_node = convert_root_query_plan_node(rust_plan); - opt_plan_node_matches(js_root_node, &rust_root_node) -} - -// Note: Reexported under `apollo_router::_private` -pub fn diff_plan(js_plan: &QueryPlanResult, rust_plan: &NativeQueryPlan) -> String { - let js_root_node = &js_plan.query_plan.node; - let rust_root_node = convert_root_query_plan_node(rust_plan); - - match (js_root_node, rust_root_node) { - (None, None) => String::from(""), - (None, Some(rust)) => { - let rust = &format!("{rust:#?}"); - let differences = diff::lines("", rust); - render_diff(&differences) - } - (Some(js), None) => { - let js = &format!("{js:#?}"); - let differences = diff::lines(js, ""); - render_diff(&differences) - } - (Some(js), Some(rust)) => { - let rust = &format!("{rust:#?}"); - let js = &format!("{js:#?}"); - let differences = diff::lines(js, rust); - render_diff(&differences) - } - } -} - -// Note: Reexported under `apollo_router::_private` -pub fn render_diff(differences: &[diff::Result<&str>]) -> String { - let mut output = String::new(); - for diff_line in differences { - match diff_line { - diff::Result::Left(l) => { - let trimmed = l.trim(); - if !trimmed.starts_with('#') && !trimmed.is_empty() { - writeln!(&mut output, "-{l}").expect("write will never fail"); - } else { - writeln!(&mut output, " {l}").expect("write will never fail"); - } - } - diff::Result::Both(l, _) => { - writeln!(&mut output, " {l}").expect("write will never fail"); - } - diff::Result::Right(r) => { - let trimmed = r.trim(); - if trimmed != "---" && !trimmed.is_empty() { - writeln!(&mut output, "+{r}").expect("write will never fail"); - } - } - } - } - output -} - -//================================================================================================== -// Vec comparison functions - -fn vec_matches(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) -> bool) -> bool { - this.len() == other.len() - && std::iter::zip(this, other).all(|(this, other)| item_matches(this, other)) -} - -fn vec_matches_result( - this: &[T], - other: &[T], - item_matches: impl Fn(&T, &T) -> Result<(), MatchFailure>, -) -> Result<(), MatchFailure> { - check_match_eq!(this.len(), other.len()); - std::iter::zip(this, other) - .enumerate() - .try_fold((), |_acc, (index, (this, other))| { - item_matches(this, other) - .map_err(|err| err.add_description(&format!("under item[{}]", index))) - })?; - Ok(()) -} - -fn vec_matches_sorted(this: &[T], other: &[T]) -> bool { - let mut this_sorted = this.to_owned(); - let mut other_sorted = other.to_owned(); - this_sorted.sort(); - other_sorted.sort(); - vec_matches(&this_sorted, &other_sorted, T::eq) -} - -fn vec_matches_sorted_by( - this: &[T], - other: &[T], - compare: impl Fn(&T, &T) -> std::cmp::Ordering, - item_matches: impl Fn(&T, &T) -> bool, -) -> bool { - let mut this_sorted = this.to_owned(); - let mut other_sorted = other.to_owned(); - this_sorted.sort_by(&compare); - other_sorted.sort_by(&compare); - vec_matches(&this_sorted, &other_sorted, item_matches) -} - -fn vec_matches_result_sorted_by( - this: &[T], - other: &[T], - compare: impl Fn(&T, &T) -> std::cmp::Ordering, - item_matches: impl Fn(&T, &T) -> Result<(), MatchFailure>, -) -> Result<(), MatchFailure> { - check_match_eq!(this.len(), other.len()); - let mut this_sorted = this.to_owned(); - let mut other_sorted = other.to_owned(); - this_sorted.sort_by(&compare); - other_sorted.sort_by(&compare); - std::iter::zip(&this_sorted, &other_sorted) - .try_fold((), |_acc, (this, other)| item_matches(this, other))?; - Ok(()) -} - -// `this` vector includes `other` vector as a set -fn vec_includes_as_set(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) -> bool) -> bool { - other.iter().all(|other_node| { - this.iter() - .any(|this_node| item_matches(this_node, other_node)) - }) -} - -// performs a set comparison, ignoring order -fn vec_matches_as_set(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) -> bool) -> bool { - // Set-inclusion test in both directions - this.len() == other.len() - && vec_includes_as_set(this, other, &item_matches) - && vec_includes_as_set(other, this, &item_matches) -} - -// Forward/reverse mappings from one Vec items (indices) to another. -type VecMapping = (HashMap, HashMap); - -// performs a set comparison, ignoring order -// and returns a mapping from `this` to `other`. -fn vec_matches_as_set_with_mapping( - this: &[T], - other: &[T], - item_matches: impl Fn(&T, &T) -> bool, -) -> VecMapping { - // Set-inclusion test in both directions - // - record forward/reverse mapping from this items <-> other items for reporting mismatches - let mut forward_map: HashMap = HashMap::new(); - let mut reverse_map: HashMap = HashMap::new(); - for (this_pos, this_node) in this.iter().enumerate() { - if let Some(other_pos) = other - .iter() - .position(|other_node| item_matches(this_node, other_node)) - { - forward_map.insert(this_pos, other_pos); - reverse_map.insert(other_pos, this_pos); - } - } - for (other_pos, other_node) in other.iter().enumerate() { - if reverse_map.contains_key(&other_pos) { - continue; - } - if let Some(this_pos) = this - .iter() - .position(|this_node| item_matches(this_node, other_node)) - { - forward_map.insert(this_pos, other_pos); - reverse_map.insert(other_pos, this_pos); - } - } - (forward_map, reverse_map) -} - -// Returns a formatted mismatch message and an optional pair of mismatched positions if the pair -// are the only remaining unmatched items. -fn format_mismatch_as_set( - this_len: usize, - other_len: usize, - forward_map: &HashMap, - reverse_map: &HashMap, -) -> Result<(String, Option<(usize, usize)>), std::fmt::Error> { - let mut ret = String::new(); - let buf = &mut ret; - write!(buf, "- mapping from left to right: [")?; - let mut this_missing_pos = None; - for this_pos in 0..this_len { - if this_pos != 0 { - write!(buf, ", ")?; - } - if let Some(other_pos) = forward_map.get(&this_pos) { - write!(buf, "{}", other_pos)?; - } else { - this_missing_pos = Some(this_pos); - write!(buf, "?")?; - } - } - writeln!(buf, "]")?; - - write!(buf, "- left-over on the right: [")?; - let mut other_missing_count = 0; - let mut other_missing_pos = None; - for other_pos in 0..other_len { - if reverse_map.get(&other_pos).is_none() { - if other_missing_count != 0 { - write!(buf, ", ")?; - } - other_missing_count += 1; - other_missing_pos = Some(other_pos); - write!(buf, "{}", other_pos)?; - } - } - write!(buf, "]")?; - let unmatched_pair = if let (Some(this_missing_pos), Some(other_missing_pos)) = - (this_missing_pos, other_missing_pos) - { - if this_len == 1 + forward_map.len() && other_len == 1 + reverse_map.len() { - // Special case: There are only one missing item on each side. They are supposed to - // match each other. - Some((this_missing_pos, other_missing_pos)) - } else { - None - } - } else { - None - }; - Ok((ret, unmatched_pair)) -} - -fn vec_matches_result_as_set( - this: &[T], - other: &[T], - item_matches: impl Fn(&T, &T) -> Result<(), MatchFailure>, -) -> Result { - // Set-inclusion test in both directions - // - record forward/reverse mapping from this items <-> other items for reporting mismatches - let (forward_map, reverse_map) = - vec_matches_as_set_with_mapping(this, other, |a, b| item_matches(a, b).is_ok()); - if forward_map.len() == this.len() && reverse_map.len() == other.len() { - Ok((forward_map, reverse_map)) - } else { - // report mismatch - let Ok((message, unmatched_pair)) = - format_mismatch_as_set(this.len(), other.len(), &forward_map, &reverse_map) - else { - // Exception: Unable to format mismatch report => fallback to most generic message - return Err(MatchFailure::new( - "mismatch at vec_matches_result_as_set (failed to format mismatched sets)" - .to_string(), - )); - }; - if let Some(unmatched_pair) = unmatched_pair { - // found a unique pair to report => use that pair's error message - let Err(err) = item_matches(&this[unmatched_pair.0], &other[unmatched_pair.1]) else { - // Exception: Unable to format unique pair mismatch error => fallback to overall report - return Err(MatchFailure::new(format!( - "mismatched sets (failed to format unique pair mismatch error):\n{}", - message - ))); - }; - Err(err.add_description(&format!( - "under a sole unmatched pair ({} -> {}) in a set comparison", - unmatched_pair.0, unmatched_pair.1 - ))) - } else { - Err(MatchFailure::new(format!("mismatched sets:\n{}", message))) - } - } -} - -//================================================================================================== -// PlanNode comparison functions - -fn option_to_string(name: Option) -> String { - name.map_or_else(|| "".to_string(), |name| name.to_string()) -} - -fn plan_node_matches(this: &PlanNode, other: &PlanNode) -> Result<(), MatchFailure> { - match (this, other) { - (PlanNode::Sequence { nodes: this }, PlanNode::Sequence { nodes: other }) => { - vec_matches_result(this, other, plan_node_matches) - .map_err(|err| err.add_description("under Sequence node"))?; - } - (PlanNode::Parallel { nodes: this }, PlanNode::Parallel { nodes: other }) => { - vec_matches_result_as_set(this, other, plan_node_matches) - .map_err(|err| err.add_description("under Parallel node"))?; - } - (PlanNode::Fetch(this), PlanNode::Fetch(other)) => { - fetch_node_matches(this, other).map_err(|err| { - err.add_description(&format!( - "under Fetch node (operation name: {})", - option_to_string(this.operation_name.as_ref()) - )) - })?; - } - (PlanNode::Flatten(this), PlanNode::Flatten(other)) => { - flatten_node_matches(this, other).map_err(|err| { - err.add_description(&format!("under Flatten node (path: {})", this.path)) - })?; - } - ( - PlanNode::Defer { primary, deferred }, - PlanNode::Defer { - primary: other_primary, - deferred: other_deferred, - }, - ) => { - defer_primary_node_matches(primary, other_primary)?; - vec_matches_result(deferred, other_deferred, deferred_node_matches)?; - } - ( - PlanNode::Subscription { primary, rest }, - PlanNode::Subscription { - primary: other_primary, - rest: other_rest, - }, - ) => { - subscription_primary_matches(primary, other_primary)?; - opt_plan_node_matches(rest, other_rest) - .map_err(|err| err.add_description("under Subscription"))?; - } - ( - PlanNode::Condition { - condition, - if_clause, - else_clause, - }, - PlanNode::Condition { - condition: other_condition, - if_clause: other_if_clause, - else_clause: other_else_clause, - }, - ) => { - check_match_eq!(condition, other_condition); - opt_plan_node_matches(if_clause, other_if_clause) - .map_err(|err| err.add_description("under Condition node (if_clause)"))?; - opt_plan_node_matches(else_clause, other_else_clause) - .map_err(|err| err.add_description("under Condition node (else_clause)"))?; - } - _ => { - return Err(MatchFailure::new(format!( - "mismatched plan node types\nleft: {:?}\nright: {:?}", - this, other - ))) - } - }; - Ok(()) -} - -pub(crate) fn opt_plan_node_matches( - this: &Option>, - other: &Option>, -) -> Result<(), MatchFailure> { - match (this, other) { - (None, None) => Ok(()), - (None, Some(_)) | (Some(_), None) => Err(MatchFailure::new(format!( - "mismatch at opt_plan_node_matches\nleft: {:?}\nright: {:?}", - this.is_some(), - other.is_some() - ))), - (Some(this), Some(other)) => plan_node_matches(this.borrow(), other.borrow()), - } -} - -fn fetch_node_matches(this: &FetchNode, other: &FetchNode) -> Result<(), MatchFailure> { - let FetchNode { - service_name, - requires, - variable_usages, - operation, - // ignored: - // reordered parallel fetches may have different names - operation_name: _, - operation_kind, - id, - input_rewrites, - output_rewrites, - context_rewrites, - // ignored - schema_aware_hash: _, - // ignored: - // when running in comparison mode, the rust plan node does not have - // the attached cache key metadata for authorisation, since the rust plan is - // not going to be the one being executed. - authorization: _, - } = this; - - check_match_eq!(*service_name, other.service_name); - check_match_eq!(*operation_kind, other.operation_kind); - check_match_eq!(*id, other.id); - check_match!(same_requires(requires, &other.requires)); - check_match!(vec_matches_sorted(variable_usages, &other.variable_usages)); - check_match!(same_rewrites(input_rewrites, &other.input_rewrites)); - check_match!(same_rewrites(output_rewrites, &other.output_rewrites)); - check_match!(same_rewrites(context_rewrites, &other.context_rewrites)); - operation_matches(operation, &other.operation)?; - Ok(()) -} - -fn subscription_primary_matches( - this: &SubscriptionNode, - other: &SubscriptionNode, -) -> Result<(), MatchFailure> { - let SubscriptionNode { - service_name, - variable_usages, - operation, - operation_name: _, // ignored (reordered parallel fetches may have different names) - operation_kind, - input_rewrites, - output_rewrites, - } = this; - check_match_eq!(*service_name, other.service_name); - check_match_eq!(*operation_kind, other.operation_kind); - check_match!(vec_matches_sorted(variable_usages, &other.variable_usages)); - check_match!(same_rewrites(input_rewrites, &other.input_rewrites)); - check_match!(same_rewrites(output_rewrites, &other.output_rewrites)); - operation_matches(operation, &other.operation)?; - Ok(()) -} - -fn defer_primary_node_matches(this: &Primary, other: &Primary) -> Result<(), MatchFailure> { - let Primary { subselection, node } = this; - opt_document_string_matches(subselection, &other.subselection) - .map_err(|err| err.add_description("under defer primary subselection"))?; - opt_plan_node_matches(node, &other.node) - .map_err(|err| err.add_description("under defer primary plan node")) -} - -fn deferred_node_matches(this: &DeferredNode, other: &DeferredNode) -> Result<(), MatchFailure> { - let DeferredNode { - depends, - label, - query_path, - subselection, - node, - } = this; - - check_match_eq!(*depends, other.depends); - check_match_eq!(*label, other.label); - check_match_eq!(*query_path, other.query_path); - opt_document_string_matches(subselection, &other.subselection) - .map_err(|err| err.add_description("under deferred subselection"))?; - opt_plan_node_matches(node, &other.node) - .map_err(|err| err.add_description("under deferred node")) -} - -fn flatten_node_matches(this: &FlattenNode, other: &FlattenNode) -> Result<(), MatchFailure> { - let FlattenNode { path, node } = this; - check_match!(same_path(path, &other.path)); - plan_node_matches(node, &other.node) -} - -fn same_path(this: &Path, other: &Path) -> bool { - // Ignore the empty key root from the JS query planner - match this.0.split_first() { - Some((PathElement::Key(k, type_conditions), rest)) - if k.is_empty() && type_conditions.is_none() => - { - vec_matches(rest, &other.0, same_path_element) - } - _ => vec_matches(&this.0, &other.0, same_path_element), - } -} - -fn same_path_element(this: &PathElement, other: &PathElement) -> bool { - match (this, other) { - (PathElement::Index(this), PathElement::Index(other)) => this == other, - (PathElement::Fragment(this), PathElement::Fragment(other)) => this == other, - ( - PathElement::Key(this_key, this_type_conditions), - PathElement::Key(other_key, other_type_conditions), - ) => { - this_key == other_key - && same_path_condition(this_type_conditions, other_type_conditions) - } - ( - PathElement::Flatten(this_type_conditions), - PathElement::Flatten(other_type_conditions), - ) => same_path_condition(this_type_conditions, other_type_conditions), - _ => false, - } -} - -fn same_path_condition(this: &Option>, other: &Option>) -> bool { - match (this, other) { - (Some(this), Some(other)) => vec_matches_sorted(this, other), - (None, None) => true, - _ => false, - } -} - -// Copied and modified from `apollo_federation::operation::SelectionKey` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -enum SelectionKey { - Field { - /// The field alias (if specified) or field name in the resulting selection set. - response_name: Name, - directives: ast::DirectiveList, - }, - FragmentSpread { - /// The name of the fragment. - fragment_name: Name, - directives: ast::DirectiveList, - }, - InlineFragment { - /// The optional type condition of the fragment. - type_condition: Option, - directives: ast::DirectiveList, - }, -} - -fn get_selection_key(selection: &Selection) -> SelectionKey { - match selection { - Selection::Field(field) => SelectionKey::Field { - response_name: field.response_name().clone(), - directives: Default::default(), - }, - Selection::InlineFragment(fragment) => SelectionKey::InlineFragment { - type_condition: fragment.type_condition.clone(), - directives: Default::default(), - }, - } -} - -fn hash_value(x: &T) -> u64 { - let mut hasher = DefaultHasher::new(); - x.hash(&mut hasher); - hasher.finish() -} - -fn hash_selection_key(selection: &Selection) -> u64 { - hash_value(&get_selection_key(selection)) -} - -// Note: This `Selection` struct is a limited version used for the `requires` field. -fn same_selection(x: &Selection, y: &Selection) -> bool { - match (x, y) { - (Selection::Field(x), Selection::Field(y)) => { - x.name == y.name - && x.alias == y.alias - && match (&x.selections, &y.selections) { - (Some(x), Some(y)) => same_selection_set_sorted(x, y), - (None, None) => true, - _ => false, - } - } - (Selection::InlineFragment(x), Selection::InlineFragment(y)) => { - x.type_condition == y.type_condition - && same_selection_set_sorted(&x.selections, &y.selections) - } - _ => false, - } -} - -fn same_selection_set_sorted(x: &[Selection], y: &[Selection]) -> bool { - fn sorted_by_selection_key(s: &[Selection]) -> Vec<&Selection> { - let mut sorted: Vec<&Selection> = s.iter().collect(); - sorted.sort_by_key(|x| hash_selection_key(x)); - sorted - } - - if x.len() != y.len() { - return false; - } - sorted_by_selection_key(x) - .into_iter() - .zip(sorted_by_selection_key(y)) - .all(|(x, y)| same_selection(x, y)) -} - -fn same_requires(x: &[Selection], y: &[Selection]) -> bool { - vec_matches_as_set(x, y, same_selection) -} - -fn same_rewrites(x: &Option>, y: &Option>) -> bool { - match (x, y) { - (None, None) => true, - (Some(x), Some(y)) => vec_matches_as_set(x, y, |a, b| a == b), - _ => false, - } -} - -fn operation_matches( - this: &SubgraphOperation, - other: &SubgraphOperation, -) -> Result<(), MatchFailure> { - document_str_matches(this.as_serialized(), other.as_serialized()) -} - -// Compare operation document strings such as query or just selection set. -fn document_str_matches(this: &str, other: &str) -> Result<(), MatchFailure> { - let this_ast = match ast::Document::parse(this, "this_operation.graphql") { - Ok(document) => document, - Err(_) => { - return Err(MatchFailure::new( - "Failed to parse this operation".to_string(), - )); - } - }; - let other_ast = match ast::Document::parse(other, "other_operation.graphql") { - Ok(document) => document, - Err(_) => { - return Err(MatchFailure::new( - "Failed to parse other operation".to_string(), - )); - } - }; - same_ast_document(&this_ast, &other_ast) -} - -fn opt_document_string_matches( - this: &Option, - other: &Option, -) -> Result<(), MatchFailure> { - match (this, other) { - (None, None) => Ok(()), - (Some(this_sel), Some(other_sel)) => document_str_matches(this_sel, other_sel), - _ => Err(MatchFailure::new(format!( - "mismatched at opt_document_string_matches\nleft: {:?}\nright: {:?}", - this, other - ))), - } -} - -//================================================================================================== -// AST comparison functions - -fn same_ast_document(x: &ast::Document, y: &ast::Document) -> Result<(), MatchFailure> { - fn split_definitions( - doc: &ast::Document, - ) -> ( - Vec<&ast::OperationDefinition>, - Vec<&ast::FragmentDefinition>, - Vec<&ast::Definition>, - ) { - let mut operations: Vec<&ast::OperationDefinition> = Vec::new(); - let mut fragments: Vec<&ast::FragmentDefinition> = Vec::new(); - let mut others: Vec<&ast::Definition> = Vec::new(); - for def in doc.definitions.iter() { - match def { - ast::Definition::OperationDefinition(op) => operations.push(op), - ast::Definition::FragmentDefinition(frag) => fragments.push(frag), - _ => others.push(def), - } - } - (operations, fragments, others) - } - - let (x_ops, x_frags, x_others) = split_definitions(x); - let (y_ops, y_frags, y_others) = split_definitions(y); - - debug_assert!(x_others.is_empty(), "Unexpected definition types"); - debug_assert!(y_others.is_empty(), "Unexpected definition types"); - debug_assert!( - x_ops.len() == y_ops.len(), - "Different number of operation definitions" - ); - - check_match_eq!(x_frags.len(), y_frags.len()); - let mut fragment_map: HashMap = HashMap::new(); - // Assumption: x_frags and y_frags are topologically sorted. - // Thus, we can build the fragment name mapping in a single pass and compare - // fragment definitions using the mapping at the same time, since earlier fragments - // will never reference later fragments. - x_frags.iter().try_fold((), |_, x_frag| { - let y_frag = y_frags - .iter() - .find(|y_frag| same_ast_fragment_definition(x_frag, y_frag, &fragment_map).is_ok()); - if let Some(y_frag) = y_frag { - if x_frag.name != y_frag.name { - // record it only if they are not identical - fragment_map.insert(x_frag.name.clone(), y_frag.name.clone()); - } - Ok(()) - } else { - Err(MatchFailure::new(format!( - "mismatch: no matching fragment definition for {}", - x_frag.name - ))) - } - })?; - - check_match_eq!(x_ops.len(), y_ops.len()); - x_ops - .iter() - .zip(y_ops.iter()) - .try_fold((), |_, (x_op, y_op)| { - same_ast_operation_definition(x_op, y_op, &fragment_map) - .map_err(|err| err.add_description("under operation definition")) - })?; - Ok(()) -} - -fn same_ast_operation_definition( - x: &ast::OperationDefinition, - y: &ast::OperationDefinition, - fragment_map: &HashMap, -) -> Result<(), MatchFailure> { - // Note: Operation names are ignored, since parallel fetches may have different names. - check_match_eq!(x.operation_type, y.operation_type); - vec_matches_result_sorted_by( - &x.variables, - &y.variables, - |a, b| a.name.cmp(&b.name), - |a, b| same_variable_definition(a, b), - ) - .map_err(|err| err.add_description("under Variable definition"))?; - check_match_eq!(x.directives, y.directives); - check_match!(same_ast_selection_set_sorted( - &x.selection_set, - &y.selection_set, - fragment_map, - )); - Ok(()) -} - -// `x` may be coerced to `y`. -// - `x` should be a value from JS QP. -// - `y` should be a value from Rust QP. -// - Assume: x and y are already checked not equal. -// Due to coercion differences, we need to compare AST values with special cases. -fn ast_value_maybe_coerced_to(x: &ast::Value, y: &ast::Value) -> bool { - match (x, y) { - // Special case 1: JS QP may convert an enum value into string. - // - In this case, compare them as strings. - (ast::Value::String(ref x), ast::Value::Enum(ref y)) => { - if x == y.as_str() { - return true; - } - } - - // Special case 2: Rust QP expands a object value by filling in its - // default field values. - // - If the Rust QP object value subsumes the JS QP object value, consider it a match. - // - Assuming the Rust QP object value has only default field values. - // - Warning: This is an unsound heuristic. - (ast::Value::Object(ref x), ast::Value::Object(ref y)) => { - if vec_includes_as_set(y, x, |(yy_name, yy_val), (xx_name, xx_val)| { - xx_name == yy_name - && (xx_val == yy_val || ast_value_maybe_coerced_to(xx_val, yy_val)) - }) { - return true; - } - } - - // Special case 3: JS QP may convert string to int for custom scalars, while Rust doesn't. - // - Note: This conversion seems a bit difficult to implement in the `apollo-federation`'s - // `coerce_value` function, since IntValue's constructor is private to the crate. - (ast::Value::Int(ref x), ast::Value::String(ref y)) => { - if x.as_str() == y { - return true; - } - } - - // Recurse into list items. - (ast::Value::List(ref x), ast::Value::List(ref y)) => { - if vec_matches(x, y, |xx, yy| { - xx == yy || ast_value_maybe_coerced_to(xx, yy) - }) { - return true; - } - } - - _ => {} // otherwise, fall through - } - false -} - -// Use this function, instead of `VariableDefinition`'s `PartialEq` implementation, -// due to known differences. -fn same_variable_definition( - x: &ast::VariableDefinition, - y: &ast::VariableDefinition, -) -> Result<(), MatchFailure> { - check_match_eq!(x.name, y.name); - check_match_eq!(x.ty, y.ty); - if x.default_value != y.default_value { - if let (Some(x), Some(y)) = (&x.default_value, &y.default_value) { - if ast_value_maybe_coerced_to(x, y) { - return Ok(()); - } - } - - return Err(MatchFailure::new(format!( - "mismatch between default values:\nleft: {:?}\nright: {:?}", - x.default_value, y.default_value - ))); - } - check_match_eq!(x.directives, y.directives); - Ok(()) -} - -fn same_ast_fragment_definition( - x: &ast::FragmentDefinition, - y: &ast::FragmentDefinition, - fragment_map: &HashMap, -) -> Result<(), MatchFailure> { - // Note: Fragment names at definitions are ignored. - check_match_eq!(x.type_condition, y.type_condition); - check_match_eq!(x.directives, y.directives); - check_match!(same_ast_selection_set_sorted( - &x.selection_set, - &y.selection_set, - fragment_map, - )); - Ok(()) -} - -fn same_ast_argument_value(x: &ast::Value, y: &ast::Value) -> bool { - x == y || ast_value_maybe_coerced_to(x, y) -} - -fn same_ast_argument(x: &ast::Argument, y: &ast::Argument) -> bool { - x.name == y.name && same_ast_argument_value(&x.value, &y.value) -} - -fn same_ast_arguments(x: &[Node], y: &[Node]) -> bool { - vec_matches_sorted_by( - x, - y, - |a, b| a.name.cmp(&b.name), - |a, b| same_ast_argument(a, b), - ) -} - -fn same_directives(x: &ast::DirectiveList, y: &ast::DirectiveList) -> bool { - vec_matches_sorted_by( - x, - y, - |a, b| a.name.cmp(&b.name), - |a, b| a.name == b.name && same_ast_arguments(&a.arguments, &b.arguments), - ) -} - -fn get_ast_selection_key( - selection: &ast::Selection, - fragment_map: &HashMap, -) -> SelectionKey { - match selection { - ast::Selection::Field(field) => SelectionKey::Field { - response_name: field.response_name().clone(), - directives: field.directives.clone(), - }, - ast::Selection::FragmentSpread(fragment) => SelectionKey::FragmentSpread { - fragment_name: fragment_map - .get(&fragment.fragment_name) - .unwrap_or(&fragment.fragment_name) - .clone(), - directives: fragment.directives.clone(), - }, - ast::Selection::InlineFragment(fragment) => SelectionKey::InlineFragment { - type_condition: fragment.type_condition.clone(), - directives: fragment.directives.clone(), - }, - } -} - -fn same_ast_selection( - x: &ast::Selection, - y: &ast::Selection, - fragment_map: &HashMap, -) -> bool { - match (x, y) { - (ast::Selection::Field(x), ast::Selection::Field(y)) => { - x.name == y.name - && x.alias == y.alias - && same_ast_arguments(&x.arguments, &y.arguments) - && same_directives(&x.directives, &y.directives) - && same_ast_selection_set_sorted(&x.selection_set, &y.selection_set, fragment_map) - } - (ast::Selection::FragmentSpread(x), ast::Selection::FragmentSpread(y)) => { - let mapped_fragment_name = fragment_map - .get(&x.fragment_name) - .unwrap_or(&x.fragment_name); - *mapped_fragment_name == y.fragment_name - && same_directives(&x.directives, &y.directives) - } - (ast::Selection::InlineFragment(x), ast::Selection::InlineFragment(y)) => { - x.type_condition == y.type_condition - && same_directives(&x.directives, &y.directives) - && same_ast_selection_set_sorted(&x.selection_set, &y.selection_set, fragment_map) - } - _ => false, - } -} - -fn hash_ast_selection_key(selection: &ast::Selection, fragment_map: &HashMap) -> u64 { - hash_value(&get_ast_selection_key(selection, fragment_map)) -} - -// Selections are sorted and compared after renaming x's fragment spreads according to the -// fragment_map. -fn same_ast_selection_set_sorted( - x: &[ast::Selection], - y: &[ast::Selection], - fragment_map: &HashMap, -) -> bool { - fn sorted_by_selection_key<'a>( - s: &'a [ast::Selection], - fragment_map: &HashMap, - ) -> Vec<&'a ast::Selection> { - let mut sorted: Vec<&ast::Selection> = s.iter().collect(); - sorted.sort_by_key(|x| hash_ast_selection_key(x, fragment_map)); - sorted - } - - if x.len() != y.len() { - return false; - } - let x_sorted = sorted_by_selection_key(x, fragment_map); // Map fragment spreads - let y_sorted = sorted_by_selection_key(y, &Default::default()); // Don't map fragment spreads - x_sorted - .into_iter() - .zip(y_sorted) - .all(|(x, y)| same_ast_selection(x, y, fragment_map)) -} - -//================================================================================================== -// Unit tests - -#[cfg(test)] -mod ast_comparison_tests { - use super::*; - - #[test] - fn test_query_variable_decl_order() { - let op_x = r#"query($qv2: String!, $qv1: Int!) { x(arg1: $qv1, arg2: $qv2) }"#; - let op_y = r#"query($qv1: Int!, $qv2: String!) { x(arg1: $qv1, arg2: $qv2) }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_query_variable_decl_enum_value_coercion() { - // Note: JS QP converts enum default values into strings. - let op_x = r#"query($qv1: E! = "default_value") { x(arg1: $qv1) }"#; - let op_y = r#"query($qv1: E! = default_value) { x(arg1: $qv1) }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_query_variable_decl_object_value_coercion_empty_case() { - // Note: Rust QP expands empty object default values by filling in its default field - // values. - let op_x = r#"query($qv1: T! = {}) { x(arg1: $qv1) }"#; - let op_y = - r#"query($qv1: T! = { field1: true, field2: "default_value" }) { x(arg1: $qv1) }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_query_variable_decl_object_value_coercion_non_empty_case() { - // Note: Rust QP expands an object default values by filling in its default field values. - let op_x = r#"query($qv1: T! = {field1: true}) { x(arg1: $qv1) }"#; - let op_y = - r#"query($qv1: T! = { field1: true, field2: "default_value" }) { x(arg1: $qv1) }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_query_variable_decl_list_of_object_value_coercion() { - // Testing a combination of list and object value coercion. - let op_x = r#"query($qv1: [T!]! = [{}]) { x(arg1: $qv1) }"#; - let op_y = - r#"query($qv1: [T!]! = [{field1: true, field2: "default_value"}]) { x(arg1: $qv1) }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_entities_selection_order() { - let op_x = r#" - query subgraph1__1($representations: [_Any!]!) { - _entities(representations: $representations) { x { w } y } - } - "#; - let op_y = r#" - query subgraph1__1($representations: [_Any!]!) { - _entities(representations: $representations) { y x { w } } - } - "#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_top_level_selection_order() { - let op_x = r#"{ x { w z } y }"#; - let op_y = r#"{ y x { z w } }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_fragment_definition_order() { - let op_x = r#"{ q { ...f1 ...f2 } } fragment f1 on T { x y } fragment f2 on T { w z }"#; - let op_y = r#"{ q { ...f1 ...f2 } } fragment f2 on T { w z } fragment f1 on T { x y }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_selection_argument_is_compared() { - let op_x = r#"{ x(arg1: "one") }"#; - let op_y = r#"{ x(arg1: "two") }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_err()); - } - - #[test] - fn test_selection_argument_order() { - let op_x = r#"{ x(arg1: "one", arg2: "two") }"#; - let op_y = r#"{ x(arg2: "two", arg1: "one") }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_selection_directive_order() { - let op_x = r#"{ x @include(if:true) @skip(if:false) }"#; - let op_y = r#"{ x @skip(if:false) @include(if:true) }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_string_to_id_coercion_difference() { - // JS QP coerces strings into integer for ID type, while Rust QP doesn't. - // This tests a special case that same_ast_document accepts this difference. - let op_x = r#"{ x(id: 123) }"#; - let op_y = r#"{ x(id: "123") }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_fragment_definition_different_names() { - let op_x = r#"{ q { ...f1 ...f2 } } fragment f1 on T { x y } fragment f2 on T { w z }"#; - let op_y = r#"{ q { ...g1 ...g2 } } fragment g1 on T { x y } fragment g2 on T { w z }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_fragment_definition_different_names_nested_1() { - // Nested fragments have the same name, only top-level fragments have different names. - let op_x = r#"{ q { ...f2 } } fragment f1 on T { x y } fragment f2 on T { z ...f1 }"#; - let op_y = r#"{ q { ...g2 } } fragment f1 on T { x y } fragment g2 on T { z ...f1 }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_fragment_definition_different_names_nested_2() { - // Nested fragments have different names. - let op_x = r#"{ q { ...f2 } } fragment f1 on T { x y } fragment f2 on T { z ...f1 }"#; - let op_y = r#"{ q { ...g2 } } fragment g1 on T { x y } fragment g2 on T { z ...g1 }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } - - #[test] - fn test_fragment_definition_different_names_nested_3() { - // Nested fragments have different names. - // Also, fragment definitions are in different order. - let op_x = r#"{ q { ...f2 ...f3 } } fragment f1 on T { x y } fragment f2 on T { z ...f1 } fragment f3 on T { w } "#; - let op_y = r#"{ q { ...g2 ...g3 } } fragment g1 on T { x y } fragment g2 on T { w } fragment g3 on T { z ...g1 }"#; - let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); - let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); - assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); - } -} - -#[cfg(test)] -mod qp_selection_comparison_tests { - use serde_json::json; - - use super::*; - - #[test] - fn test_requires_comparison_with_same_selection_key() { - let requires_json = json!([ - { - "kind": "InlineFragment", - "typeCondition": "T", - "selections": [ - { - "kind": "Field", - "name": "id", - }, - ] - }, - { - "kind": "InlineFragment", - "typeCondition": "T", - "selections": [ - { - "kind": "Field", - "name": "id", - }, - { - "kind": "Field", - "name": "job", - } - ] - }, - ]); - - // The only difference between requires1 and requires2 is the order of selections. - // But, their items all have the same SelectionKey. - let requires1: Vec = serde_json::from_value(requires_json).unwrap(); - let requires2: Vec = requires1.iter().rev().cloned().collect(); - - // `same_selection_set_sorted` fails to match, since it doesn't account for - // two items with the same SelectionKey but in different order. - assert!(!same_selection_set_sorted(&requires1, &requires2)); - // `same_requires` should succeed. - assert!(same_requires(&requires1, &requires2)); - } -} - -#[cfg(test)] -mod path_comparison_tests { - use serde_json::json; - - use super::*; - - macro_rules! matches_deserialized_path { - ($json:expr, $expected:expr) => { - let path: Path = serde_json::from_value($json).unwrap(); - assert_eq!(path, $expected); - }; - } - - #[test] - fn test_type_condition_deserialization() { - matches_deserialized_path!( - json!(["k"]), - Path(vec![PathElement::Key("k".to_string(), None)]) - ); - matches_deserialized_path!( - json!(["k|[A]"]), - Path(vec![PathElement::Key( - "k".to_string(), - Some(vec!["A".to_string()]) - )]) - ); - matches_deserialized_path!( - json!(["k|[A,B]"]), - Path(vec![PathElement::Key( - "k".to_string(), - Some(vec!["A".to_string(), "B".to_string()]) - )]) - ); - matches_deserialized_path!( - json!(["k|[]"]), - Path(vec![PathElement::Key("k".to_string(), Some(vec![]))]) - ); - } - - macro_rules! assert_path_match { - ($a:expr, $b:expr) => { - let legacy_path: Path = serde_json::from_value($a).unwrap(); - let native_path: Path = serde_json::from_value($b).unwrap(); - assert!(same_path(&legacy_path, &native_path)); - }; - } - - macro_rules! assert_path_differ { - ($a:expr, $b:expr) => { - let legacy_path: Path = serde_json::from_value($a).unwrap(); - let native_path: Path = serde_json::from_value($b).unwrap(); - assert!(!same_path(&legacy_path, &native_path)); - }; - } - - #[test] - fn test_same_path_basic() { - // Basic dis-equality tests. - assert_path_differ!(json!([]), json!(["a"])); - assert_path_differ!(json!(["a"]), json!(["b"])); - assert_path_differ!(json!(["a", "b"]), json!(["a", "b", "c"])); - } - - #[test] - fn test_same_path_ignore_empty_root_key() { - assert_path_match!(json!(["", "k|[A]", "v"]), json!(["k|[A]", "v"])); - } - - #[test] - fn test_same_path_distinguishes_empty_conditions_from_no_conditions() { - // Create paths that use no type conditions and empty type conditions - assert_path_differ!(json!(["k|[]", "v"]), json!(["k", "v"])); - } -} diff --git a/apollo-router/src/query_planner/selection.rs b/apollo-router/src/query_planner/selection.rs index cf58be1244..8550a8d655 100644 --- a/apollo-router/src/query_planner/selection.rs +++ b/apollo-router/src/query_planner/selection.rs @@ -39,13 +39,6 @@ pub(crate) struct Field { pub(crate) selections: Option>, } -impl Field { - // Mirroring `apollo_compiler::Field::response_name` - pub(crate) fn response_name(&self) -> &Name { - self.alias.as_ref().unwrap_or(&self.name) - } -} - /// An inline fragment. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] 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 7b2e0c912f..cbac25c490 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 @@ -7,7 +7,7 @@ Fetch( service_name: "accounts", requires: [], variable_usages: [], - operation: "{me{name{first last}}}", + operation: "{ me { name { first last } } }", operation_name: None, operation_kind: Query, id: None, @@ -15,7 +15,7 @@ Fetch( output_rewrites: None, context_rewrites: None, schema_aware_hash: QueryHash( - "65e550250ef331b8dc49d9e2da8f4cd5add979720cbe83ba545a0f78ece8d329", + "16d9399fe60cb6dcc81f522fab6a54056e8b65b02095667b76f1cdd7048aab50", ), authorization: CacheKeyMetadata { is_authenticated: false, diff --git a/apollo-router/src/query_planner/tests.rs b/apollo-router/src/query_planner/tests.rs index 766ce9a715..b22204f7cd 100644 --- a/apollo-router/src/query_planner/tests.rs +++ b/apollo-router/src/query_planner/tests.rs @@ -6,7 +6,6 @@ use std::sync::Arc; use apollo_compiler::name; use futures::StreamExt; use http::Method; -use router_bridge::planner::UsageReporting; use serde_json_bytes::json; use tokio_stream::wrappers::ReceiverStream; use tower::ServiceExt; @@ -18,6 +17,7 @@ use super::OperationKind; use super::PlanNode; use super::Primary; use super::QueryPlan; +use crate::apollo_studio_interop::UsageReporting; use crate::graphql; use crate::json_ext::Path; use crate::json_ext::PathElement; diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index d34eae8b45..46f4d37a7f 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -295,20 +295,10 @@ impl YamlRouterFactory { ) -> Result { let query_planner_span = tracing::info_span!("query_planner_creation"); // QueryPlannerService takes an UnplannedRequest and outputs PlannedRequest - let bridge_query_planner = BridgeQueryPlannerPool::new( - previous_supergraph - .as_ref() - .map(|router| router.js_planners()) - .unwrap_or_default(), - schema.clone(), - configuration.clone(), - configuration - .supergraph - .query_planning - .experimental_query_planner_parallelism()?, - ) - .instrument(query_planner_span) - .await?; + let bridge_query_planner = + BridgeQueryPlannerPool::new(schema.clone(), configuration.clone()) + .instrument(query_planner_span) + .await?; let schema_changed = previous_supergraph .map(|supergraph_creator| supergraph_creator.schema().raw_sdl == schema.raw_sdl) @@ -498,12 +488,9 @@ pub async fn create_test_service_factory_from_yaml(schema: &str, configuration: .await; assert_eq!( service.map(|_| ()).unwrap_err().to_string().as_str(), - r#"couldn't build Query Planner Service: couldn't instantiate query planner; invalid schema: schema validation errors: Unexpected error extracting subgraphs from the supergraph: this is either a bug, or the supergraph has been corrupted. + r#"failed to initialize the query planner: An internal error has occurred, please report this bug to Apollo. -Details: -Error: Cannot find type "Review" in subgraph "products" -caused by -"# +Details: Object field "Product.reviews"'s inner type "Review" does not refer to an existing output type."# ); } diff --git a/apollo-router/src/services/layers/query_analysis.rs b/apollo-router/src/services/layers/query_analysis.rs index 2e2fe77eff..fbafb49a51 100644 --- a/apollo-router/src/services/layers/query_analysis.rs +++ b/apollo-router/src/services/layers/query_analysis.rs @@ -11,11 +11,11 @@ use apollo_compiler::ExecutableDocument; use apollo_compiler::Node; use http::StatusCode; use lru::LruCache; -use router_bridge::planner::UsageReporting; use tokio::sync::Mutex; use crate::apollo_studio_interop::generate_extended_references; use crate::apollo_studio_interop::ExtendedReferenceStats; +use crate::apollo_studio_interop::UsageReporting; use crate::compute_job; use crate::context::OPERATION_KIND; use crate::context::OPERATION_NAME; diff --git a/apollo-router/src/services/query_planner.rs b/apollo-router/src/services/query_planner.rs index d23a0e2ace..ce4675f884 100644 --- a/apollo-router/src/services/query_planner.rs +++ b/apollo-router/src/services/query_planner.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use async_trait::async_trait; use derivative::Derivative; -use router_bridge::planner::PlanOptions; use serde::Deserialize; use serde::Serialize; use static_assertions::assert_impl_all; @@ -15,6 +14,14 @@ use crate::graphql; use crate::query_planner::QueryPlan; use crate::Context; +/// Options for planning a query +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)] +#[serde(rename_all = "camelCase")] +pub(crate) struct PlanOptions { + /// Which labels to override during query planning + pub(crate) override_conditions: Vec, +} + assert_impl_all!(Request: Send); /// [`Context`] for the request. #[derive(Derivative)] diff --git a/apollo-router/src/services/supergraph/service.rs b/apollo-router/src/services/supergraph/service.rs index b059c7aa07..5a057bb85c 100644 --- a/apollo-router/src/services/supergraph/service.rs +++ b/apollo-router/src/services/supergraph/service.rs @@ -12,8 +12,6 @@ use http::StatusCode; use indexmap::IndexMap; use opentelemetry::Key; use opentelemetry::KeyValue; -use router_bridge::planner::Planner; -use router_bridge::planner::UsageReporting; use tokio::sync::mpsc; use tokio::sync::mpsc::error::SendError; use tokio_stream::wrappers::ReceiverStream; @@ -26,6 +24,7 @@ use tracing::field; use tracing::Span; use tracing_futures::Instrument; +use crate::apollo_studio_interop::UsageReporting; use crate::batching::BatchQuery; use crate::configuration::Batching; use crate::configuration::PersistedQueriesPrewarmQueryPlanCache; @@ -49,7 +48,6 @@ use crate::query_planner::subscription::SUBSCRIPTION_EVENT_SPAN_NAME; use crate::query_planner::BridgeQueryPlannerPool; use crate::query_planner::CachingQueryPlanner; use crate::query_planner::InMemoryCachePlanner; -use crate::query_planner::QueryPlanResult; use crate::router_factory::create_plugins; use crate::router_factory::create_subgraph_services; use crate::services::execution::QueryPlan; @@ -928,10 +926,6 @@ impl SupergraphCreator { self.query_planner_service.previous_cache() } - pub(crate) fn js_planners(&self) -> Vec>> { - self.query_planner_service.js_planners() - } - pub(crate) async fn warm_up_query_planner( &mut self, query_parser: &QueryAnalysisLayer, diff --git a/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_query_planner_mode.router.yaml b/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_query_planner_mode.router.yaml deleted file mode 100644 index 195306ed62..0000000000 --- a/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_query_planner_mode.router.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# This config updates the query plan options so that we can see if there is a different redis cache entry generted for query plans -supergraph: - query_planning: - cache: - redis: - required_to_start: true - urls: - - redis://localhost:6379 - ttl: 10s - -experimental_query_planner_mode: new \ No newline at end of file diff --git a/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml b/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml deleted file mode 100644 index 6f24cebc9a..0000000000 --- a/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# This config updates the query plan options so that we can see if there is a different redis cache entry generted for query plans -supergraph: - query_planning: - cache: - redis: - required_to_start: true - urls: - - redis://localhost:6379 - ttl: 10s - experimental_reuse_query_fragments: true - generate_query_fragments: false -experimental_query_planner_mode: legacy \ No newline at end of file diff --git a/apollo-router/tests/integration/query_planner.rs b/apollo-router/tests/integration/query_planner.rs index d2834bfd84..179ca1df9a 100644 --- a/apollo-router/tests/integration/query_planner.rs +++ b/apollo-router/tests/integration/query_planner.rs @@ -6,47 +6,11 @@ use crate::integration::IntegrationTest; mod max_evaluated_plans; const PROMETHEUS_METRICS_CONFIG: &str = include_str!("telemetry/fixtures/prometheus.router.yaml"); -const LEGACY_QP: &str = "experimental_query_planner_mode: legacy"; -const NEW_QP: &str = "experimental_query_planner_mode: new"; -const BOTH_QP: &str = "experimental_query_planner_mode: both"; -const BOTH_BEST_EFFORT_QP: &str = "experimental_query_planner_mode: both_best_effort"; -const NEW_BEST_EFFORT_QP: &str = "experimental_query_planner_mode: new_best_effort"; - -#[tokio::test(flavor = "multi_thread")] -async fn fed1_schema_with_legacy_qp() { - let mut router = IntegrationTest::builder() - .config(LEGACY_QP) - .supergraph("../examples/graphql/supergraph-fed1.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} #[tokio::test(flavor = "multi_thread")] async fn fed1_schema_with_new_qp() { let mut router = IntegrationTest::builder() - .config(NEW_QP) - .supergraph("../examples/graphql/supergraph-fed1.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "could not create router: \ - failed to initialize the query planner: \ - Supergraphs composed with federation version 1 are not supported.", - ) - .await; - router.assert_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn fed1_schema_with_both_qp() { - let mut router = IntegrationTest::builder() - .config(BOTH_QP) + .config("{}") // Default config .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; @@ -61,112 +25,10 @@ async fn fed1_schema_with_both_qp() { router.assert_shutdown().await; } -#[tokio::test(flavor = "multi_thread")] -async fn fed1_schema_with_both_best_effort_qp() { - let mut router = IntegrationTest::builder() - .config(BOTH_BEST_EFFORT_QP) - .supergraph("../examples/graphql/supergraph-fed1.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "Falling back to the legacy query planner: \ - failed to initialize the query planner: \ - Supergraphs composed with federation version 1 are not supported. \ - Please recompose your supergraph with federation version 2 or greater", - ) - .await; - router.assert_started().await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn fed1_schema_with_new_best_effort_qp() { - let mut router = IntegrationTest::builder() - .config(NEW_BEST_EFFORT_QP) - .supergraph("../examples/graphql/supergraph-fed1.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "Falling back to the legacy query planner: \ - failed to initialize the query planner: \ - Supergraphs composed with federation version 1 are not supported. \ - Please recompose your supergraph with federation version 2 or greater", - ) - .await; - router.assert_started().await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn fed1_schema_with_legacy_qp_reload_to_new_keep_previous_config() { - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); - let mut router = IntegrationTest::builder() - .config(config) - .supergraph("../examples/graphql/supergraph-fed1.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); - router.update_config(&config).await; - router - .assert_log_contains("error while reloading, continuing with previous configuration") - .await; - router - .assert_metrics_contains( - r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="fed1",init_is_success="false",otel_scope_name="apollo/router"} 1"#, - None, - ) - .await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn fed1_schema_with_legacy_qp_reload_to_both_best_effort_keep_previous_config() { - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); - let mut router = IntegrationTest::builder() - .config(config) - .supergraph("../examples/graphql/supergraph-fed1.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{BOTH_BEST_EFFORT_QP}"); - router.update_config(&config).await; - router - .assert_log_contains( - "Falling back to the legacy query planner: \ - failed to initialize the query planner: \ - Supergraphs composed with federation version 1 are not supported. \ - Please recompose your supergraph with federation version 2 or greater", - ) - .await; - router - .assert_metrics_contains( - r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="fed1",init_is_success="false",otel_scope_name="apollo/router"} 1"#, - None, - ) - .await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - #[tokio::test(flavor = "multi_thread")] async fn fed2_schema_with_new_qp() { - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); let mut router = IntegrationTest::builder() - .config(config) + .config(PROMETHEUS_METRICS_CONFIG) .supergraph("../examples/graphql/supergraph.graphql") .build() .await; @@ -182,29 +44,13 @@ async fn fed2_schema_with_new_qp() { router.graceful_shutdown().await; } -#[tokio::test(flavor = "multi_thread")] -async fn context_with_legacy_qp() { - if !graph_os_enabled() { - return; - } - let mut router = IntegrationTest::builder() - .config(PROMETHEUS_METRICS_CONFIG) - .supergraph("tests/fixtures/set_context/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - #[tokio::test(flavor = "multi_thread")] async fn context_with_new_qp() { if !graph_os_enabled() { return; } let mut router = IntegrationTest::builder() - .config(NEW_QP) + .config("{}") // Default config .supergraph("tests/fixtures/set_context/supergraph.graphql") .build() .await; @@ -214,61 +60,10 @@ async fn context_with_new_qp() { router.graceful_shutdown().await; } -#[tokio::test(flavor = "multi_thread")] -async fn invalid_schema_with_legacy_qp_fails_startup() { - let mut router = IntegrationTest::builder() - .config(LEGACY_QP) - .supergraph("tests/fixtures/broken-supergraph.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "could not create router: \ - Federation error: Invalid supergraph: must be a core schema", - ) - .await; - router.assert_shutdown().await; -} - #[tokio::test(flavor = "multi_thread")] async fn invalid_schema_with_new_qp_fails_startup() { let mut router = IntegrationTest::builder() - .config(NEW_QP) - .supergraph("tests/fixtures/broken-supergraph.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "could not create router: \ - Federation error: Invalid supergraph: must be a core schema", - ) - .await; - router.assert_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn invalid_schema_with_both_qp_fails_startup() { - let mut router = IntegrationTest::builder() - .config(BOTH_QP) - .supergraph("tests/fixtures/broken-supergraph.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "could not create router: \ - Federation error: Invalid supergraph: must be a core schema", - ) - .await; - router.assert_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn invalid_schema_with_both_best_effort_qp_fails_startup() { - let mut router = IntegrationTest::builder() - .config(BOTH_BEST_EFFORT_QP) + .config("{}") // Default config .supergraph("tests/fixtures/broken-supergraph.graphql") .build() .await; @@ -284,9 +79,8 @@ async fn invalid_schema_with_both_best_effort_qp_fails_startup() { #[tokio::test(flavor = "multi_thread")] async fn valid_schema_with_new_qp_change_to_broken_schema_keeps_old_config() { - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); let mut router = IntegrationTest::builder() - .config(config) + .config(PROMETHEUS_METRICS_CONFIG) .supergraph("tests/fixtures/valid-supergraph.graphql") .build() .await; diff --git a/apollo-router/tests/integration/query_planner/max_evaluated_plans.rs b/apollo-router/tests/integration/query_planner/max_evaluated_plans.rs index d6474aa30b..4e55f37757 100644 --- a/apollo-router/tests/integration/query_planner/max_evaluated_plans.rs +++ b/apollo-router/tests/integration/query_planner/max_evaluated_plans.rs @@ -49,52 +49,11 @@ async fn reports_evaluated_plans() { router.graceful_shutdown().await; } -#[tokio::test(flavor = "multi_thread")] -async fn does_not_exceed_max_evaluated_plans_legacy() { - let mut router = IntegrationTest::builder() - .config( - r#" - experimental_query_planner_mode: legacy - telemetry: - exporters: - metrics: - prometheus: - enabled: true - supergraph: - query_planning: - experimental_plans_limit: 4 - "#, - ) - .supergraph("tests/integration/fixtures/query_planner_max_evaluated_plans.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router - .execute_query(&json!({ - "query": r#"{ t { v1 v2 v3 v4 } }"#, - "variables": {}, - })) - .await; - - let metrics = router - .get_metrics_response() - .await - .expect("failed to fetch metrics") - .text() - .await - .expect("metrics are not text?!"); - assert_evaluated_plans(&metrics, 4); - - router.graceful_shutdown().await; -} - #[tokio::test(flavor = "multi_thread")] async fn does_not_exceed_max_evaluated_plans() { let mut router = IntegrationTest::builder() .config( r#" - experimental_query_planner_mode: new telemetry: exporters: metrics: diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index f5e3da3438..b7fb1cbf58 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -1,6 +1,5 @@ // The redis cache keys in this file have to change whenever the following change: // * the supergraph schema -// * experimental_query_planner_mode // * federation version // // How to get the new cache key: @@ -52,7 +51,7 @@ async fn query_planner_cache() -> Result<(), BoxError> { // If this test fails and the cache key format changed you'll need to update the key here. // Look at the top of the file for instructions on getting the new cache key. let known_cache_key = &format!( - "plan:router:{}:8c0b4bfb4630635c2b5748c260d686ddb301d164e5818c63d6d9d77e13631676:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:924b36a9ae6af4ff198220b1302b14b6329c4beb7c022fd31d6fef82eaad7ccb", + "plan:router:{}:8c0b4bfb4630635c2b5748c260d686ddb301d164e5818c63d6d9d77e13631676:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:d9f7a00bc249cb51cfc8599f86b6dc5272967b37b1409dc4717f105b6939fe43", env!("CARGO_PKG_VERSION") ); @@ -992,23 +991,13 @@ async fn query_planner_redis_update_query_fragments() { // This configuration turns the fragment generation option *off*. include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), &format!( - "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:28acec2bebc3922cd261ed3c8a13b26d53b49e891797a199e3e1ce8089e813e6", + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:fb1a8e6e454ad6a1d0d48b24dc9c7c4dd6d9bf58b6fdaf43cd24eb77fbbb3a17", env!("CARGO_PKG_VERSION") ), ) .await; } -#[tokio::test(flavor = "multi_thread")] -#[ignore = "the cache key for different query planner modes is currently different"] -async fn query_planner_redis_update_planner_mode() { - test_redis_query_plan_config_update( - include_str!("fixtures/query_planner_redis_config_update_query_planner_mode.router.yaml"), - "", - ) - .await; -} - #[tokio::test(flavor = "multi_thread")] async fn query_planner_redis_update_defer() { // If this test fails and the cache key format changed you'll need to update @@ -1025,7 +1014,7 @@ async fn query_planner_redis_update_defer() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), &format!( - "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:af3139ddd647c755d2eab5e6a177dc443030a528db278c19ad7b45c5c0324378", + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:dc062fcc9cfd9582402d1e8b1fa3ee336ea1804d833443869e0b3744996716a2", env!("CARGO_PKG_VERSION") ), ) @@ -1050,33 +1039,7 @@ async fn query_planner_redis_update_type_conditional_fetching() { "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), &format!( - "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:8b87abe2e45d38df4712af966aa540f33dbab6fc2868a409f2dbb6a5a4fb2d08", - env!("CARGO_PKG_VERSION") - ), - ) - .await; -} - -// TODO drop this test once we remove the JS QP -#[tokio::test(flavor = "multi_thread")] -async fn query_planner_redis_update_reuse_query_fragments() { - // If this test fails and the cache key format changed you'll need to update - // the key here. Look at the top of the file for instructions on getting - // the new cache key. - // - // You first need to follow the process and update the key in - // `test_redis_query_plan_config_update`, and then update the key in this - // test. - // - // This test requires graphos license, so make sure you have - // "TEST_APOLLO_KEY" and "TEST_APOLLO_GRAPH_REF" env vars set, otherwise the - // test just passes locally. - test_redis_query_plan_config_update( - include_str!( - "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" - ), - &format!( - "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:9af18c8afd568c197050fc1a60c52a8c98656f1775016110516fabfbedc135fe", + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:bdc09980aa6ef28a67f5aeb8759763d8ac5a4fc43afa8c5a89f58cc998c48db3", env!("CARGO_PKG_VERSION") ), ) @@ -1104,7 +1067,7 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key // If the tests above are failing, this is the key that needs to be changed first. let starting_key = &format!( - "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:924b36a9ae6af4ff198220b1302b14b6329c4beb7c022fd31d6fef82eaad7ccb", + "plan:router:{}:5938623f2155169070684a48be1e0b8468d0f2c662b5527a2247f683173f7d05:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:d9f7a00bc249cb51cfc8599f86b6dc5272967b37b1409dc4717f105b6939fe43", env!("CARGO_PKG_VERSION") ); assert_ne!(starting_key, new_cache_key, "starting_key (cache key for the initial config) and new_cache_key (cache key with the updated config) should not be equal. This either means that the cache key is not being generated correctly, or that the test is not actually checking the updated key."); diff --git a/apollo-router/tests/integration/telemetry/metrics.rs b/apollo-router/tests/integration/telemetry/metrics.rs index cbce509c13..cd9c1c4550 100644 --- a/apollo-router/tests/integration/telemetry/metrics.rs +++ b/apollo-router/tests/integration/telemetry/metrics.rs @@ -273,24 +273,6 @@ async fn test_gauges_on_reload() { router .assert_metrics_contains(r#"apollo_router_cache_storage_estimated_size{kind="query planner",type="memory",otel_scope_name="apollo/router"} "#, None) .await; - router - .assert_metrics_contains( - r#"apollo_router_query_planning_queued{otel_scope_name="apollo/router"} "#, - None, - ) - .await; - router - .assert_metrics_contains( - r#"apollo_router_v8_heap_total_bytes{otel_scope_name="apollo/router"} "#, - None, - ) - .await; - router - .assert_metrics_contains( - r#"apollo_router_v8_heap_total_bytes{otel_scope_name="apollo/router"} "#, - None, - ) - .await; router .assert_metrics_contains( r#"apollo_router_cache_size{kind="APQ",type="memory",otel_scope_name="apollo/router"} 1"#, diff --git a/apollo-router/tests/samples/enterprise/progressive-override/basic/configuration.yaml b/apollo-router/tests/samples/enterprise/progressive-override/basic/configuration.yaml index 321fd80abd..3f64e39a39 100644 --- a/apollo-router/tests/samples/enterprise/progressive-override/basic/configuration.yaml +++ b/apollo-router/tests/samples/enterprise/progressive-override/basic/configuration.yaml @@ -7,7 +7,5 @@ telemetry: stdout: format: text -experimental_query_planner_mode: legacy - plugins: experimental.expose_query_plan: true \ No newline at end of file diff --git a/apollo-router/tests/samples/enterprise/progressive-override/basic/configuration2.yaml b/apollo-router/tests/samples/enterprise/progressive-override/basic/configuration2.yaml index 7c445ce5b2..44ce15d711 100644 --- a/apollo-router/tests/samples/enterprise/progressive-override/basic/configuration2.yaml +++ b/apollo-router/tests/samples/enterprise/progressive-override/basic/configuration2.yaml @@ -7,8 +7,6 @@ telemetry: stdout: format: text -experimental_query_planner_mode: legacy - rhai: scripts: "tests/samples/enterprise/progressive-override/basic/rhai" main: "main.rhai" diff --git a/apollo-router/tests/samples/enterprise/progressive-override/basic/plan.json b/apollo-router/tests/samples/enterprise/progressive-override/basic/plan.json index 6cdf9d7b60..6bcbdecf5a 100644 --- a/apollo-router/tests/samples/enterprise/progressive-override/basic/plan.json +++ b/apollo-router/tests/samples/enterprise/progressive-override/basic/plan.json @@ -11,7 +11,7 @@ { "request": { "body": { - "query": "query progressive1__Subgraph1__0{percent100{__typename id}}", + "query": "query progressive1__Subgraph1__0 { percent100 { __typename id } }", "operationName": "progressive1__Subgraph1__0" } }, @@ -33,7 +33,7 @@ { "request": { "body": { - "query": "query progressive1__Subgraph2__1($representations:[_Any!]!){_entities(representations:$representations){...on T{foo}}}", + "query": "query progressive1__Subgraph2__1($representations: [_Any!]!) { _entities(representations: $representations) { ... on T { foo } } }", "operationName": "progressive1__Subgraph2__1", "variables": { "representations": [ @@ -60,7 +60,7 @@ { "request": { "body": { - "query": "query progressive2__Subgraph2__0{percent0{foo}}", + "query": "query progressive2__Subgraph2__0 { percent0 { foo } }", "operationName": "progressive2__Subgraph2__0" } }, @@ -119,7 +119,7 @@ { "request": { "body": { - "query": "query progressive3__Subgraph1__0{percent100{__typename id}}", + "query": "query progressive3__Subgraph1__0 { percent100 { __typename id } }", "operationName": "progressive3__Subgraph1__0" } }, @@ -137,7 +137,7 @@ { "request": { "body": { - "query": "query progressive4__Subgraph1__0{percent100{bar}}", + "query": "query progressive4__Subgraph1__0 { percent100 { bar } }", "operationName": "progressive4__Subgraph1__0" } }, @@ -158,7 +158,7 @@ { "request": { "body": { - "query": "query progressive3__Subgraph2__1($representations:[_Any!]!){_entities(representations:$representations){...on T{bar}}}", + "query": "query progressive3__Subgraph2__1($representations: [_Any!]!) { _entities(representations: $representations) { ... on T { bar } } }", "operationName": "progressive3__Subgraph2__1", "variables": { "representations": [ diff --git a/apollo-router/tests/samples/enterprise/progressive-override/warmup/configuration.yaml b/apollo-router/tests/samples/enterprise/progressive-override/warmup/configuration.yaml index b069d5af18..8d54c2ee26 100644 --- a/apollo-router/tests/samples/enterprise/progressive-override/warmup/configuration.yaml +++ b/apollo-router/tests/samples/enterprise/progressive-override/warmup/configuration.yaml @@ -11,7 +11,5 @@ telemetry: stdout: format: text -experimental_query_planner_mode: legacy - plugins: experimental.expose_query_plan: true \ No newline at end of file diff --git a/apollo-router/tests/samples/enterprise/progressive-override/warmup/configuration2.yaml b/apollo-router/tests/samples/enterprise/progressive-override/warmup/configuration2.yaml index 413d26aba3..fa34365889 100644 --- a/apollo-router/tests/samples/enterprise/progressive-override/warmup/configuration2.yaml +++ b/apollo-router/tests/samples/enterprise/progressive-override/warmup/configuration2.yaml @@ -11,8 +11,6 @@ telemetry: stdout: format: text -experimental_query_planner_mode: legacy - # rhai: # scripts: "tests/samples/enterprise/progressive-override/rhai" # main: "main.rhai" diff --git a/apollo-router/tests/samples/enterprise/progressive-override/warmup/plan.json b/apollo-router/tests/samples/enterprise/progressive-override/warmup/plan.json index 8f913eb5be..288a933d4b 100644 --- a/apollo-router/tests/samples/enterprise/progressive-override/warmup/plan.json +++ b/apollo-router/tests/samples/enterprise/progressive-override/warmup/plan.json @@ -11,7 +11,7 @@ { "request": { "body": { - "query": "query progressive1__Subgraph1__0{percent100{__typename id}}", + "query": "query progressive1__Subgraph1__0 { percent100 { __typename id } }", "operationName": "progressive1__Subgraph1__0" } }, @@ -33,7 +33,7 @@ { "request": { "body": { - "query": "query progressive1__Subgraph2__1($representations:[_Any!]!){_entities(representations:$representations){...on T{foo}}}", + "query": "query progressive1__Subgraph2__1($representations: [_Any!]!) { _entities(representations: $representations) { ... on T { foo } } }", "operationName": "progressive1__Subgraph2__1", "variables": { "representations": [ @@ -60,7 +60,7 @@ { "request": { "body": { - "query": "query progressive2__Subgraph2__0{percent0{foo}}", + "query": "query progressive2__Subgraph2__0 { percent0 { foo } }", "operationName": "progressive2__Subgraph2__0" } }, @@ -106,7 +106,7 @@ { "request": { "body": { - "query": "query progressive1__Subgraph1__0{percent100{__typename id}}", + "query": "query progressive1__Subgraph1__0 { percent100 { __typename id } }", "operationName": "progressive1__Subgraph1__0" } }, @@ -128,7 +128,7 @@ { "request": { "body": { - "query": "query progressive1__Subgraph2__1($representations:[_Any!]!){_entities(representations:$representations){...on T{foo}}}", + "query": "query progressive1__Subgraph2__1($representations: [_Any!]!) { _entities(representations: $representations) { ... on T { foo } } }", "operationName": "progressive1__Subgraph2__1", "variables": { "representations": [ @@ -155,7 +155,7 @@ { "request": { "body": { - "query": "query progressive2__Subgraph2__0{percent0{foo}}", + "query": "query progressive2__Subgraph2__0 { percent0 { foo } }", "operationName": "progressive2__Subgraph2__0" } }, diff --git a/apollo-router/tests/set_context.rs b/apollo-router/tests/set_context.rs index 96086bbe4c..6ebb9e590b 100644 --- a/apollo-router/tests/set_context.rs +++ b/apollo-router/tests/set_context.rs @@ -32,26 +32,8 @@ macro_rules! snap } } -fn get_configuration(rust_qp: bool) -> serde_json::Value { - if rust_qp { - return json! {{ - "experimental_query_planner_mode": "new", - "experimental_type_conditioned_fetching": true, - // will make debugging easier - "plugins": { - "experimental.expose_query_plan": true - }, - "include_subgraph_errors": { - "all": true - }, - "supergraph": { - // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 - "generate_query_fragments": false, - } - }}; - } +fn get_configuration() -> serde_json::Value { json! {{ - "experimental_query_planner_mode": "legacy", "experimental_type_conditioned_fetching": true, // will make debugging easier "plugins": { @@ -67,12 +49,8 @@ fn get_configuration(rust_qp: bool) -> serde_json::Value { }} } -async fn run_single_request( - query: &str, - rust_qp: bool, - mocks: &[(&'static str, &'static str)], -) -> Response { - let configuration = get_configuration(rust_qp); +async fn run_single_request(query: &str, mocks: &[(&'static str, &'static str)]) -> Response { + let configuration = get_configuration(); let harness = setup_from_mocks(configuration, mocks); let supergraph_service = harness.build_supergraph().await.unwrap(); let request = supergraph::Request::fake_builder() @@ -91,33 +69,6 @@ async fn run_single_request( .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, - false, - &[ - ("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_rust_qp() { static QUERY: &str = r#" @@ -134,32 +85,6 @@ async fn test_set_context_rust_qp() { let response = run_single_request( QUERY, - true, - &[ - ("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, - false, &[ ("Subgraph1", include_str!("fixtures/set_context/one.json")), ("Subgraph2", include_str!("fixtures/set_context/two.json")), @@ -184,32 +109,6 @@ async fn test_set_context_no_typenames_rust_qp() { let response = run_single_request( QUERY_NO_TYPENAMES, - true, - &[ - ("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, - false, &[ ("Subgraph1", include_str!("fixtures/set_context/one.json")), ("Subgraph2", include_str!("fixtures/set_context/two.json")), @@ -234,32 +133,6 @@ async fn test_set_context_list_rust_qp() { let response = run_single_request( QUERY_WITH_LIST, - true, - &[ - ("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, - false, &[ ("Subgraph1", include_str!("fixtures/set_context/one.json")), ("Subgraph2", include_str!("fixtures/set_context/two.json")), @@ -284,38 +157,6 @@ async fn test_set_context_list_of_lists_rust_qp() { let response = run_single_request( QUERY_WITH_LIST_OF_LISTS, - true, - &[ - ("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, - false, &[ ("Subgraph1", include_str!("fixtures/set_context/one.json")), ("Subgraph2", include_str!("fixtures/set_context/two.json")), @@ -346,7 +187,6 @@ async fn test_set_context_union_rust_qp() { let response = run_single_request( QUERY_WITH_UNION, - true, &[ ("Subgraph1", include_str!("fixtures/set_context/one.json")), ("Subgraph2", include_str!("fixtures/set_context/two.json")), @@ -357,34 +197,6 @@ async fn test_set_context_union_rust_qp() { 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, - false, - &[ - ( - "Subgraph1", - include_str!("fixtures/set_context/one_null_param.json"), - ), - ("Subgraph2", include_str!("fixtures/set_context/two.json")), - ], - ) - .await; - - insta::assert_json_snapshot!(response); -} - #[tokio::test(flavor = "multi_thread")] async fn test_set_context_with_null_rust_qp() { static QUERY: &str = r#" @@ -399,7 +211,6 @@ async fn test_set_context_with_null_rust_qp() { let response = run_single_request( QUERY, - true, &[ ( "Subgraph1", @@ -413,33 +224,6 @@ async fn test_set_context_with_null_rust_qp() { 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, - false, - &[ - ("Subgraph1", include_str!("fixtures/set_context/one.json")), - ("Subgraph2", include_str!("fixtures/set_context/two.json")), - ], - ) - .await; - - snap!(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")] @@ -456,7 +240,6 @@ async fn test_set_context_type_mismatch_rust_qp() { let response = run_single_request( QUERY, - true, &[ ("Subgraph1", include_str!("fixtures/set_context/one.json")), ("Subgraph2", include_str!("fixtures/set_context/two.json")), @@ -467,37 +250,6 @@ async fn test_set_context_type_mismatch_rust_qp() { 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, - false, - &[ - ( - "Subgraph1", - include_str!("fixtures/set_context/one_fetch_failure.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")] @@ -515,7 +267,6 @@ async fn test_set_context_unrelated_fetch_failure_rust_qp() { let response = run_single_request( QUERY, - true, &[ ( "Subgraph1", @@ -529,36 +280,6 @@ async fn test_set_context_unrelated_fetch_failure_rust_qp() { 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, - false, - &[ - ( - "Subgraph1", - include_str!("fixtures/set_context/one_dependent_fetch_failure.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")] @@ -575,7 +296,6 @@ async fn test_set_context_dependent_fetch_failure_rust_qp() { let response = run_single_request( QUERY, - true, &[ ( "Subgraph1", diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap index de7f4d2827..4d61ae95b4 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "18631e67bb0c6b514cb51e8dff155a2900c8000ad319ea4784e5ca8b1275aca2", + "schemaAwareHash": "1d2d8b1ab80b4b1293e9753a915997835e6ff5bc54ba4c9b400abe7fa4661386", "authorization": { "is_authenticated": false, "scopes": [], @@ -137,7 +137,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7f3ec4c2c644d43e54d95da83790166d87ab6bfcb31fe5692d8262199bff6d3f", + "schemaAwareHash": "66a2bd39c499f1edd8c3ec1bfbc170cb995c6f9e23427b5486b633decd2da08b", "authorization": { "is_authenticated": false, "scopes": [], @@ -148,7 +148,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n \n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n \n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n \n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Flatten(path: \"search.@.sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n\n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n\n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n\n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Flatten(path: \"search.@.sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap index 39b73eabbc..c89cab9c7d 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "18631e67bb0c6b514cb51e8dff155a2900c8000ad319ea4784e5ca8b1275aca2", + "schemaAwareHash": "1d2d8b1ab80b4b1293e9753a915997835e6ff5bc54ba4c9b400abe7fa4661386", "authorization": { "is_authenticated": false, "scopes": [], @@ -92,9 +92,8 @@ expression: response { "kind": "Flatten", "path": [ - "", "search", - "@|[MovieResult]", + "@|[ArticleResult]", "sections", "@" ], @@ -104,7 +103,7 @@ expression: response "requires": [ { "kind": "InlineFragment", - "typeCondition": "EntityCollectionSection", + "typeCondition": "GallerySection", "selections": [ { "kind": "Field", @@ -118,7 +117,7 @@ expression: response }, { "kind": "InlineFragment", - "typeCondition": "GallerySection", + "typeCondition": "EntityCollectionSection", "selections": [ { "kind": "Field", @@ -132,16 +131,16 @@ expression: response } ], "variableUsages": [ - "movieResultParam" + "articleResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7f3ec4c2c644d43e54d95da83790166d87ab6bfcb31fe5692d8262199bff6d3f", + "schemaAwareHash": "31465e7b7e358ea9407067188249b51fd7342088e6084360ed0df28199cef5cc", "authorization": { "is_authenticated": false, "scopes": [], @@ -152,9 +151,8 @@ expression: response { "kind": "Flatten", "path": [ - "", "search", - "@|[ArticleResult]", + "@|[MovieResult]", "sections", "@" ], @@ -164,7 +162,7 @@ expression: response "requires": [ { "kind": "InlineFragment", - "typeCondition": "GallerySection", + "typeCondition": "EntityCollectionSection", "selections": [ { "kind": "Field", @@ -178,7 +176,7 @@ expression: response }, { "kind": "InlineFragment", - "typeCondition": "EntityCollectionSection", + "typeCondition": "GallerySection", "selections": [ { "kind": "Field", @@ -192,16 +190,16 @@ expression: response } ], "variableUsages": [ - "articleResultParam" + "movieResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "3874fd9db4a0302422701b93506f42a5de5604355be7093fa2abe23f440161f9", + "schemaAwareHash": "550ad525da9bb9497fb0d51bf7a64b7d5d73ade5ee7d2e425573dc7e2e248e99", "authorization": { "is_authenticated": false, "scopes": [], @@ -214,7 +212,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n \n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n \n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n \n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \".search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n Flatten(path: \".search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n\n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n\n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n\n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \"search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n Flatten(path: \"search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap index 39b73eabbc..c89cab9c7d 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap @@ -79,7 +79,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "18631e67bb0c6b514cb51e8dff155a2900c8000ad319ea4784e5ca8b1275aca2", + "schemaAwareHash": "1d2d8b1ab80b4b1293e9753a915997835e6ff5bc54ba4c9b400abe7fa4661386", "authorization": { "is_authenticated": false, "scopes": [], @@ -92,9 +92,8 @@ expression: response { "kind": "Flatten", "path": [ - "", "search", - "@|[MovieResult]", + "@|[ArticleResult]", "sections", "@" ], @@ -104,7 +103,7 @@ expression: response "requires": [ { "kind": "InlineFragment", - "typeCondition": "EntityCollectionSection", + "typeCondition": "GallerySection", "selections": [ { "kind": "Field", @@ -118,7 +117,7 @@ expression: response }, { "kind": "InlineFragment", - "typeCondition": "GallerySection", + "typeCondition": "EntityCollectionSection", "selections": [ { "kind": "Field", @@ -132,16 +131,16 @@ expression: response } ], "variableUsages": [ - "movieResultParam" + "articleResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7f3ec4c2c644d43e54d95da83790166d87ab6bfcb31fe5692d8262199bff6d3f", + "schemaAwareHash": "31465e7b7e358ea9407067188249b51fd7342088e6084360ed0df28199cef5cc", "authorization": { "is_authenticated": false, "scopes": [], @@ -152,9 +151,8 @@ expression: response { "kind": "Flatten", "path": [ - "", "search", - "@|[ArticleResult]", + "@|[MovieResult]", "sections", "@" ], @@ -164,7 +162,7 @@ expression: response "requires": [ { "kind": "InlineFragment", - "typeCondition": "GallerySection", + "typeCondition": "EntityCollectionSection", "selections": [ { "kind": "Field", @@ -178,7 +176,7 @@ expression: response }, { "kind": "InlineFragment", - "typeCondition": "EntityCollectionSection", + "typeCondition": "GallerySection", "selections": [ { "kind": "Field", @@ -192,16 +190,16 @@ expression: response } ], "variableUsages": [ - "articleResultParam" + "movieResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "3874fd9db4a0302422701b93506f42a5de5604355be7093fa2abe23f440161f9", + "schemaAwareHash": "550ad525da9bb9497fb0d51bf7a64b7d5d73ade5ee7d2e425573dc7e2e248e99", "authorization": { "is_authenticated": false, "scopes": [], @@ -214,7 +212,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n \n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n \n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n \n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \".search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n Flatten(path: \".search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n\n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n\n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n\n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \"search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n Flatten(path: \"search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap index ec86110080..371fd3496e 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap @@ -141,7 +141,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "65c1648beef44b81ac988224191b18ff469c641fd33032ef0c84165245018b62", + "schemaAwareHash": "b74616ae898acf3abefb83e24bde5faf0de0f9475d703b105b60c18c7372ab13", "authorization": { "is_authenticated": false, "scopes": [], @@ -154,10 +154,9 @@ expression: response { "kind": "Flatten", "path": [ - "", "searchListOfList", "@", - "@|[MovieResult]", + "@|[ArticleResult]", "sections", "@" ], @@ -167,7 +166,7 @@ expression: response "requires": [ { "kind": "InlineFragment", - "typeCondition": "EntityCollectionSection", + "typeCondition": "GallerySection", "selections": [ { "kind": "Field", @@ -181,7 +180,7 @@ expression: response }, { "kind": "InlineFragment", - "typeCondition": "GallerySection", + "typeCondition": "EntityCollectionSection", "selections": [ { "kind": "Field", @@ -195,16 +194,16 @@ expression: response } ], "variableUsages": [ - "movieResultParam" + "articleResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7f3ec4c2c644d43e54d95da83790166d87ab6bfcb31fe5692d8262199bff6d3f", + "schemaAwareHash": "31465e7b7e358ea9407067188249b51fd7342088e6084360ed0df28199cef5cc", "authorization": { "is_authenticated": false, "scopes": [], @@ -215,10 +214,9 @@ expression: response { "kind": "Flatten", "path": [ - "", "searchListOfList", "@", - "@|[ArticleResult]", + "@|[MovieResult]", "sections", "@" ], @@ -228,7 +226,7 @@ expression: response "requires": [ { "kind": "InlineFragment", - "typeCondition": "GallerySection", + "typeCondition": "EntityCollectionSection", "selections": [ { "kind": "Field", @@ -242,7 +240,7 @@ expression: response }, { "kind": "InlineFragment", - "typeCondition": "EntityCollectionSection", + "typeCondition": "GallerySection", "selections": [ { "kind": "Field", @@ -256,16 +254,16 @@ expression: response } ], "variableUsages": [ - "articleResultParam" + "movieResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "3874fd9db4a0302422701b93506f42a5de5604355be7093fa2abe23f440161f9", + "schemaAwareHash": "550ad525da9bb9497fb0d51bf7a64b7d5d73ade5ee7d2e425573dc7e2e248e99", "authorization": { "is_authenticated": false, "scopes": [], @@ -278,7 +276,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfList {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n \n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n \n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n \n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \".searchListOfList.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n Flatten(path: \".searchListOfList.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfList {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n\n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n\n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n\n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \"searchListOfList.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n Flatten(path: \"searchListOfList.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap index e4ba5927a3..354bd034a9 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap @@ -145,7 +145,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "f2466229a91f69cadfa844a20343b03668b7f85fd1310a4b20ba9382ffa2f5e7", + "schemaAwareHash": "dc1df8e8d701876c6ea7d25bbeab92a5629a82e55660ccc48fc37e12d5157efa", "authorization": { "is_authenticated": false, "scopes": [], @@ -158,11 +158,10 @@ expression: response { "kind": "Flatten", "path": [ - "", "searchListOfListOfList", "@", "@", - "@|[MovieResult]", + "@|[ArticleResult]", "sections", "@" ], @@ -172,7 +171,7 @@ expression: response "requires": [ { "kind": "InlineFragment", - "typeCondition": "EntityCollectionSection", + "typeCondition": "GallerySection", "selections": [ { "kind": "Field", @@ -186,7 +185,7 @@ expression: response }, { "kind": "InlineFragment", - "typeCondition": "GallerySection", + "typeCondition": "EntityCollectionSection", "selections": [ { "kind": "Field", @@ -200,16 +199,16 @@ expression: response } ], "variableUsages": [ - "movieResultParam" + "articleResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7f3ec4c2c644d43e54d95da83790166d87ab6bfcb31fe5692d8262199bff6d3f", + "schemaAwareHash": "31465e7b7e358ea9407067188249b51fd7342088e6084360ed0df28199cef5cc", "authorization": { "is_authenticated": false, "scopes": [], @@ -220,11 +219,10 @@ expression: response { "kind": "Flatten", "path": [ - "", "searchListOfListOfList", "@", "@", - "@|[ArticleResult]", + "@|[MovieResult]", "sections", "@" ], @@ -234,7 +232,7 @@ expression: response "requires": [ { "kind": "InlineFragment", - "typeCondition": "GallerySection", + "typeCondition": "EntityCollectionSection", "selections": [ { "kind": "Field", @@ -248,7 +246,7 @@ expression: response }, { "kind": "InlineFragment", - "typeCondition": "EntityCollectionSection", + "typeCondition": "GallerySection", "selections": [ { "kind": "Field", @@ -262,16 +260,16 @@ expression: response } ], "variableUsages": [ - "articleResultParam" + "movieResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "3874fd9db4a0302422701b93506f42a5de5604355be7093fa2abe23f440161f9", + "schemaAwareHash": "550ad525da9bb9497fb0d51bf7a64b7d5d73ade5ee7d2e425573dc7e2e248e99", "authorization": { "is_authenticated": false, "scopes": [], @@ -284,7 +282,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfListOfList {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n \n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n \n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n \n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \".searchListOfListOfList.@.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n Flatten(path: \".searchListOfListOfList.@.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfListOfList {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n\n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n\n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n\n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \"searchListOfListOfList.@.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n Flatten(path: \"searchListOfListOfList.@.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap index 8ae0b59f67..8811454f74 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap @@ -54,7 +54,7 @@ expression: response "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "cc52bb826d3c06b3ccbc421340fe3f49a81dc2b71dcb6a931a9a769745038e3f", + "schemaAwareHash": "446c1a72168f736a89e4f56799333e05b26092d36fc55e22c2e92828061c787b", "authorization": { "is_authenticated": false, "scopes": [], @@ -67,9 +67,8 @@ expression: response { "kind": "Flatten", "path": [ - "", "search", - "@|[MovieResult]", + "@|[ArticleResult]", "sections", "@" ], @@ -79,7 +78,7 @@ expression: response "requires": [ { "kind": "InlineFragment", - "typeCondition": "EntityCollectionSection", + "typeCondition": "GallerySection", "selections": [ { "kind": "Field", @@ -93,7 +92,7 @@ expression: response }, { "kind": "InlineFragment", - "typeCondition": "GallerySection", + "typeCondition": "EntityCollectionSection", "selections": [ { "kind": "Field", @@ -107,16 +106,16 @@ expression: response } ], "variableUsages": [ - "movieResultParam" + "articleResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "6e83e0a67b509381f1a0c2dfe84db92d0dd6bf4bb23fe4c97ccd3d871364c9f4", + "schemaAwareHash": "f9052a9ce97a084006a1f2054b7e0fba8734f24bb53cf0f7e0ba573c7e709b98", "authorization": { "is_authenticated": false, "scopes": [], @@ -127,9 +126,8 @@ expression: response { "kind": "Flatten", "path": [ - "", "search", - "@|[ArticleResult]", + "@|[MovieResult]", "sections", "@" ], @@ -139,7 +137,7 @@ expression: response "requires": [ { "kind": "InlineFragment", - "typeCondition": "GallerySection", + "typeCondition": "EntityCollectionSection", "selections": [ { "kind": "Field", @@ -153,7 +151,7 @@ expression: response }, { "kind": "InlineFragment", - "typeCondition": "EntityCollectionSection", + "typeCondition": "GallerySection", "selections": [ { "kind": "Field", @@ -167,16 +165,16 @@ expression: response } ], "variableUsages": [ - "articleResultParam" + "movieResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "67834874c123139d942b140fb9ff00ed4e22df25228c3e758eeb44b28d3847eb", + "schemaAwareHash": "027cac0584184439636aea68757da18f3e0e18142948e3b8625724f93e8720fc", "authorization": { "is_authenticated": false, "scopes": [], @@ -189,7 +187,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \".search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n Flatten(path: \".search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n Flatten(path: \"search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/type_conditions.rs b/apollo-router/tests/type_conditions.rs index ab43b3f521..09466bee5d 100644 --- a/apollo-router/tests/type_conditions.rs +++ b/apollo-router/tests/type_conditions.rs @@ -30,45 +30,38 @@ struct RequestAndResponse { #[tokio::test(flavor = "multi_thread")] async fn test_type_conditions_enabled() { - _test_type_conditions_enabled("legacy").await; - _test_type_conditions_enabled("new").await; + _test_type_conditions_enabled().await; } #[tokio::test(flavor = "multi_thread")] async fn test_type_conditions_enabled_generate_query_fragments() { - _test_type_conditions_enabled_generate_query_fragments("legacy").await; - _test_type_conditions_enabled_generate_query_fragments("new").await; + _test_type_conditions_enabled_generate_query_fragments().await; } #[tokio::test(flavor = "multi_thread")] async fn test_type_conditions_enabled_list_of_list() { - _test_type_conditions_enabled_list_of_list("legacy").await; - _test_type_conditions_enabled_list_of_list("new").await; + _test_type_conditions_enabled_list_of_list().await; } #[tokio::test(flavor = "multi_thread")] async fn test_type_conditions_enabled_list_of_list_of_list() { - _test_type_conditions_enabled_list_of_list_of_list("legacy").await; - _test_type_conditions_enabled_list_of_list_of_list("new").await; + _test_type_conditions_enabled_list_of_list_of_list().await; } #[tokio::test(flavor = "multi_thread")] async fn test_type_conditions_disabled() { - _test_type_conditions_disabled("legacy").await; - _test_type_conditions_disabled("new").await; + _test_type_conditions_disabled().await; } #[tokio::test(flavor = "multi_thread")] async fn test_type_conditions_enabled_shouldnt_make_article_fetch() { - _test_type_conditions_enabled_shouldnt_make_article_fetch("legacy").await; - _test_type_conditions_enabled_shouldnt_make_article_fetch("new").await; + _test_type_conditions_enabled_shouldnt_make_article_fetch().await; } -async fn _test_type_conditions_enabled(planner_mode: &str) -> Response { +async fn _test_type_conditions_enabled() -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, - "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -113,11 +106,10 @@ async fn _test_type_conditions_enabled(planner_mode: &str) -> Response { response } -async fn _test_type_conditions_enabled_generate_query_fragments(planner_mode: &str) -> Response { +async fn _test_type_conditions_enabled_generate_query_fragments() -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, - "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -162,11 +154,10 @@ async fn _test_type_conditions_enabled_generate_query_fragments(planner_mode: &s response } -async fn _test_type_conditions_enabled_list_of_list(planner_mode: &str) -> Response { +async fn _test_type_conditions_enabled_list_of_list() -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, - "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -212,11 +203,10 @@ async fn _test_type_conditions_enabled_list_of_list(planner_mode: &str) -> Respo } // one last to make sure unnesting is correct -async fn _test_type_conditions_enabled_list_of_list_of_list(planner_mode: &str) -> Response { +async fn _test_type_conditions_enabled_list_of_list_of_list() -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, - "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -261,11 +251,10 @@ async fn _test_type_conditions_enabled_list_of_list_of_list(planner_mode: &str) response } -async fn _test_type_conditions_disabled(planner_mode: &str) -> Response { +async fn _test_type_conditions_disabled() -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": false, - "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -309,11 +298,10 @@ async fn _test_type_conditions_disabled(planner_mode: &str) -> Response { response } -async fn _test_type_conditions_enabled_shouldnt_make_article_fetch(planner_mode: &str) -> Response { +async fn _test_type_conditions_enabled_shouldnt_make_article_fetch() -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, - "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true diff --git a/docs/source/reference/router/configuration.mdx b/docs/source/reference/router/configuration.mdx index 2de7a04ecb..4b6747f7af 100644 --- a/docs/source/reference/router/configuration.mdx +++ b/docs/source/reference/router/configuration.mdx @@ -579,64 +579,6 @@ You can configure certain caching behaviors for generated query plans and APQ (b - You can configure a Redis-backed _distributed_ cache that enables multiple router instances to share cached values. For details, see [Distributed caching in the GraphOS Router](/router/configuration/distributed-caching/). - You can configure a Redis-backed _entity_ cache that enables a client query to retrieve cached entity data split between subgraph reponses. For details, see [Subgraph entity caching in the GraphOS Router](/router/configuration/entity-caching/). - - -### Native query planner - - - -Starting with v1.49.0, the router can run a Rust-native query planner. This native query planner can be run by itself to plan all queries, replacing the legacy JavaScript implementation. - - - -Starting with v1.57.0, to run the most performant and resource-efficient native query planner and to disable the V8 JavaScript runtime in the router, set the following options in your `router.yaml`: - -```yaml title="router.yaml" -experimental_query_planner_mode: new -``` - -You can also improve throughput by reducing the size of queries sent to subgraphs with the following option: - -```yaml title="router.yaml" -supergraph: - generate_query_fragments: true -``` - - - -Learn more in [Native Query Planner](/router/executing-operations/native-query-planner) docs. - - - -### Query planner pools - - - - - -You can improve the performance of the router's query planner by configuring parallelized query planning. - -By default, the query planner plans one operation at a time. It plans one operation to completion before planning the next one. This serial planning can be problematic when an operation takes a long time to plan and consequently blocks the query planner from working on other operations. - -To resolve such blocking scenarios, you can enable parallel query planning. Configure it in `router.yaml` with `supergraph.query_planning.experimental_parallelism`: - -```yaml title="router.yaml" -supergraph: - query_planning: - experimental_parallelism: auto # number of available cpus -``` - -The value of `experimental_parallelism` is the number of query planners in the router's _query planner pool_. A query planner pool is a preallocated set of query planners from which the router can use to plan operations. The total number of pools is the maximum number of query planners that can run in parallel and therefore the maximum number of operations that can be worked on simultaneously. - -Valid values of `experimental_parallelism`: -- Any integer starting from `1` -- The special value `auto`, which sets the number of query planners equal to the number of available CPUs on the router's host machine - -The default value of `experimental_parallelism` is `1`. - -In practice, you should tune `experimental_parallelism` based on metrics and benchmarks gathered from your router. - ### Enhanced operation signature normalization @@ -1273,26 +1215,6 @@ supergraph: generate_query_fragments: false ``` - - -The legacy query planner still supports an experimental algorithm that attempts to -reuse fragments from the original operation while forming subgraph requests. The -legacy query planner has to be explicitly enabled. This experimental feature used to -be enabled by default, but is still available to support subgraphs that rely on the -specific shape of fragments in an operation: - -```yaml -supergraph: - generate_query_fragments: false - experimental_reuse_query_fragments: true -``` - -Note that `generate_query_fragments` and `experimental_reuse_query_fragments` are -mutually exclusive; if both are explicitly set to `true`, `generate_query_fragments` -will take precedence. - - - ### Reusing configuration You can reuse parts of your configuration file in multiple places using standard YAML aliasing syntax: diff --git a/docs/source/routing/about-router.mdx b/docs/source/routing/about-router.mdx index 47c71d0de6..2439d70b39 100644 --- a/docs/source/routing/about-router.mdx +++ b/docs/source/routing/about-router.mdx @@ -145,5 +145,3 @@ Cloud-hosted routers automatically have access to additional GraphOS Router feat - For all available configuration options, go to [Router configuration](/graphos/reference/router/configuration) reference docs - To learn more about the intricacies of query plans, see the [example graph](/graphos/reference/federation/query-plans#example-graph) and [query plan](/graphos/reference/federation/query-plans#example-graph) in reference docs - -- For the most performant query planning, configure and use the [Rust-native query planner](/graphos/routing/query-planning/native-query-planner). diff --git a/docs/source/routing/performance/query-planner-pools.mdx b/docs/source/routing/performance/query-planner-pools.mdx deleted file mode 100644 index e278a1e030..0000000000 --- a/docs/source/routing/performance/query-planner-pools.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Query Planner Pools -subtitle: Run multiple query planners in parallel -minVersion: 1.44.0 -redirectFrom: - - /router/configuration/overview/#query-planner-pools ---- - - - -You can improve the performance of the router's query planner by configuring parallelized query planning. - -By default, the query planner plans one operation at a time. It plans one operation to completion before planning the next one. This serial planning can be problematic when an operation takes a long time to plan and consequently blocks the query planner from working on other operations. - -## Configuring query planner pools - -To resolve such blocking scenarios, you can enable parallel query planning. Configure it in `router.yaml` with `supergraph.query_planning.experimental_parallelism`: - -```yaml title="router.yaml" -supergraph: - query_planning: - experimental_parallelism: auto # number of available cpus -``` - -The value of `experimental_parallelism` is the number of query planners in the router's _query planner pool_. A query planner pool is a preallocated set of query planners from which the router can use to plan operations. The total number of pools is the maximum number of query planners that can run in parallel and therefore the maximum number of operations that can be worked on simultaneously. - -Valid values of `experimental_parallelism`: -- Any integer starting from `1` -- The special value `auto`, which sets the number of query planners equal to the number of available CPUs on the router's host machine - -The default value of `experimental_parallelism` is `1`. - -In practice, you should tune `experimental_parallelism` based on metrics and benchmarks gathered from your router. \ No newline at end of file diff --git a/docs/source/routing/query-planning/native-query-planner.mdx b/docs/source/routing/query-planning/native-query-planner.mdx deleted file mode 100644 index 7da2d5a099..0000000000 --- a/docs/source/routing/query-planning/native-query-planner.mdx +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Native Query Planner -subtitle: Run the Rust-native query planner in GraphOS Router -minVersion: 1.49.0 -redirectFrom: - - /router/configuration/experimental_query_planner_mode - - /router/executing-operations/native-query-planner ---- - - - -Learn to run the GraphOS Router with the Rust-native query planner and improve your query planning performance and scalability. - -## Background about query planner implementations - -In v1.49.0 the router introduced a [query planner](/graphos/routing/about-router#query-planning) implemented natively in Rust. This native query planner improves the overall performance and resource utilization of query planning. It exists alongside the legacy JavaScript implementation that uses the V8 JavaScript engine, and it will eventually replace the legacy implementation. - -### Comparing query planner implementations - -As part of the effort to ensure correctness and stability of the new query planner, starting in v1.53.0 the router enables both the new and legacy planners and runs them in parallel to compare their results by default. After their comparison, the router discards the native query planner's results and uses only the legacy planner to execute requests. The native query planner uses a single thread in the cold path of the router. It has a bounded queue of ten queries. If the queue is full, the router simply does not run the comparison to avoid excessive resource consumption. - -## Configuring query planning - -You can configure the `experimental_query_planner_mode` option in your `router.yaml` to set the query planner to run. - -The `experimental_query_planner_mode` option has the following supported modes: - -- `new`- enables only the new Rust-native query planner -- `legacy` - enables only the legacy JavaScript query planner -- `both_best_effort` (default) - enables both new and legacy query planners for comparison. The legacy query planner is used for execution. - - - -## Optimize native query planner - - - -To run the native query planner with the best performance and resource utilization, configure your router with the following options: - -```yaml title="router.yaml" -experimental_query_planner_mode: new -``` - - - -In router v1.56, running the native query planner with the best performance and resource utilization also requires setting `experimental_introspection_mode: new`. - - - -Setting `experimental_query_planner_mode: new` not only enables native query planning and schema introspection, it also disables the V8 JavaScript runtime used by the legacy query planner. Disabling V8 frees up CPU and memory and improves native query planning performance. - -Additionally, to enable more optimal native query planning and faster throughput by reducing the size of queries sent to subgraphs, you can enable query fragment generation with the following option: - -```yaml title="router.yaml" -supergraph: - generate_query_fragments: true -``` - - - -Regarding [fragment reuse and generation](/router/configuration/overview#fragment-reuse-and-generation), in the future the `generate_query_fragments` option will be the only option for handling fragments. - - - -## Metrics for native query planner - -When running both query planners for comparison with `experimental_query_planner_mode: both_best_effort`, the following metrics track mismatches and errors: - -- `apollo.router.operations.query_planner.both` with the following attributes: - - `generation.is_matched` (bool) - - `generation.js_error` (bool) - - `generation.rust_error` (bool) - -- `apollo.router.query_planning.plan.duration` with the following attributes to differentiate between planners: - - `planner` (rust | js) - -## Limitations of native query planner - -The native query planner doesn't implement `@context`. This is planned to be implemented in a future router release. diff --git a/fuzz/router.yaml b/fuzz/router.yaml index 6756683b4e..def8d82978 100644 --- a/fuzz/router.yaml +++ b/fuzz/router.yaml @@ -1,7 +1,6 @@ supergraph: listen: 0.0.0.0:4000 introspection: true -experimental_query_planner_mode: both sandbox: enabled: true homepage: From 8171f3bf8717fa054cd2d0eba14ec995dbd63b27 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Thu, 12 Dec 2024 17:43:11 +0100 Subject: [PATCH 17/35] Update Rust toolchain to 1.83.0 (#6444) Co-authored-by: Coenen Benjamin --- Cargo.lock | 1 - apollo-federation/src/display_helpers.rs | 2 +- .../src/link/context_spec_definition.rs | 13 +- .../src/link/cost_spec_definition.rs | 13 +- .../src/link/federation_spec_definition.rs | 4 - .../src/link/inaccessible_spec_definition.rs | 24 +- .../src/link/join_spec_definition.rs | 33 +-- .../src/link/link_spec_definition.rs | 15 +- apollo-federation/src/link/mod.rs | 6 +- apollo-federation/src/link/spec_definition.rs | 18 -- apollo-federation/src/merge.rs | 6 +- .../src/operation/directive_list.rs | 4 +- apollo-federation/src/operation/merging.rs | 6 +- apollo-federation/src/operation/mod.rs | 6 +- apollo-federation/src/operation/optimize.rs | 12 - .../src/operation/selection_map.rs | 2 +- apollo-federation/src/operation/simplify.rs | 3 +- apollo-federation/src/operation/tests/mod.rs | 20 +- .../src/query_graph/graph_path.rs | 18 +- apollo-federation/src/query_graph/mod.rs | 4 +- .../src/query_graph/path_tree.rs | 1 + .../src/query_plan/fetch_dependency_graph.rs | 13 +- apollo-federation/src/query_plan/generate.rs | 2 +- .../query_plan/query_planning_traversal.rs | 4 +- apollo-federation/src/schema/mod.rs | 2 + .../type_and_directive_specification.rs | 2 - apollo-federation/src/supergraph/mod.rs | 1 - .../src/utils/fallible_iterator.rs | 244 +----------------- apollo-federation/src/utils/logging.rs | 2 +- .../scaffold-test/Dockerfile | 2 +- .../templates/base/Dockerfile | 2 +- .../templates/base/rust-toolchain.toml | 2 +- apollo-router/Cargo.toml | 2 - apollo-router/README.md | 2 +- apollo-router/src/ageing_priority_queue.rs | 2 +- .../src/apollo_studio_interop/mod.rs | 2 +- .../axum_factory/axum_http_server_factory.rs | 2 +- apollo-router/src/batching.rs | 58 +++-- apollo-router/src/configuration/mod.rs | 4 + apollo-router/src/configuration/subgraph.rs | 2 +- apollo-router/src/context/extensions/sync.rs | 2 +- apollo-router/src/graphql/request.rs | 4 +- apollo-router/src/http_ext.rs | 5 +- apollo-router/src/json_ext.rs | 20 +- apollo-router/src/lib.rs | 2 - apollo-router/src/metrics/layer.rs | 6 +- apollo-router/src/metrics/mod.rs | 1 - apollo-router/src/plugin/serde.rs | 10 +- .../src/plugins/authentication/jwks.rs | 2 +- .../plugins/authorization/authenticated.rs | 6 +- .../src/plugins/authorization/policy.rs | 6 +- .../src/plugins/authorization/scopes.rs | 6 +- .../src/plugins/cache/cache_control.rs | 12 +- .../src/plugins/cache/invalidation.rs | 3 - .../cost_calculator/static_cost.rs | 2 +- .../src/plugins/demand_control/mod.rs | 2 +- apollo-router/src/plugins/fleet_detector.rs | 3 +- apollo-router/src/plugins/headers.rs | 2 +- .../src/plugins/include_subgraph_errors.rs | 4 +- .../plugins/progressive_override/visitor.rs | 2 +- .../telemetry/config_new/attributes.rs | 2 +- .../telemetry/config_new/graphql/mod.rs | 2 +- .../src/plugins/telemetry/endpoint.rs | 4 +- .../src/plugins/telemetry/fmt_layer.rs | 6 +- .../src/plugins/telemetry/formatters/json.rs | 10 +- .../src/plugins/telemetry/formatters/text.rs | 10 +- .../metrics/span_metrics_exporter.rs | 2 +- .../src/plugins/telemetry/otel/layer.rs | 6 +- .../telemetry/tracing/apollo_telemetry.rs | 1 + .../src/plugins/telemetry/tracing/datadog.rs | 2 +- .../datadog_exporter/exporter/intern.rs | 6 +- .../tracing/datadog_exporter/exporter/mod.rs | 19 -- .../exporter/model/unified_tags.rs | 2 +- apollo-router/src/plugins/telemetry/utils.rs | 16 -- apollo-router/src/query_planner/execution.rs | 2 +- .../src/query_planner/subgraph_context.rs | 4 +- .../src/query_planner/subscription.rs | 2 +- apollo-router/src/services/external.rs | 4 + .../src/services/layers/query_analysis.rs | 3 - apollo-router/src/services/subgraph.rs | 3 + .../src/services/subgraph_service.rs | 32 +-- apollo-router/src/spec/field_type.rs | 2 +- apollo-router/src/spec/query/change.rs | 26 +- apollo-router/src/spec/query/subselections.rs | 2 +- apollo-router/src/spec/query/transform.rs | 2 +- .../persisted_queries_manifest_stream.rs | 1 - apollo-router/src/uplink/schema_stream.rs | 1 - apollo-router/tests/common.rs | 1 + .../tests/integration/telemetry/jaeger.rs | 6 +- dockerfiles/diy/dockerfiles/Dockerfile.repo | 2 +- .../routing/customization/custom-binary.mdx | 2 +- examples/async-auth/rust/src/main.rs | 2 + .../rust/src/main.rs | 2 + .../rust/src/propagate_status_code.rs | 4 +- fuzz/subgraph/src/model.rs | 168 ++++++------ rust-toolchain.toml | 4 +- 96 files changed, 309 insertions(+), 715 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5805ff09d8..1fb43a1848 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,7 +365,6 @@ dependencies = [ "reqwest", "rhai", "rmp", - "rowan", "rstack", "rust-embed", "rustls", diff --git a/apollo-federation/src/display_helpers.rs b/apollo-federation/src/display_helpers.rs index 898e7efd3e..a88e683549 100644 --- a/apollo-federation/src/display_helpers.rs +++ b/apollo-federation/src/display_helpers.rs @@ -67,7 +67,7 @@ pub(crate) fn write_indented_lines( pub(crate) struct DisplaySlice<'a, T>(pub(crate) &'a [T]); -impl<'a, T: Display> Display for DisplaySlice<'a, T> { +impl Display for DisplaySlice<'_, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "[")?; let mut iter = self.0.iter(); diff --git a/apollo-federation/src/link/context_spec_definition.rs b/apollo-federation/src/link/context_spec_definition.rs index 5008bc328c..70ae8af6ad 100644 --- a/apollo-federation/src/link/context_spec_definition.rs +++ b/apollo-federation/src/link/context_spec_definition.rs @@ -25,17 +25,15 @@ pub(crate) struct ContextDirectiveArguments<'doc> { #[derive(Clone)] pub(crate) struct ContextSpecDefinition { url: Url, - minimum_federation_version: Option, } impl ContextSpecDefinition { - pub(crate) fn new(version: Version, minimum_federation_version: Option) -> Self { + pub(crate) fn new(version: Version) -> Self { Self { url: Url { identity: Identity::context_identity(), version, }, - minimum_federation_version, } } @@ -61,19 +59,12 @@ impl SpecDefinition for ContextSpecDefinition { fn url(&self) -> &Url { &self.url } - - fn minimum_federation_version(&self) -> Option<&Version> { - self.minimum_federation_version.as_ref() - } } lazy_static! { pub(crate) static ref CONTEXT_VERSIONS: SpecDefinitions = { let mut definitions = SpecDefinitions::new(Identity::context_identity()); - definitions.add(ContextSpecDefinition::new( - Version { major: 0, minor: 1 }, - Some(Version { major: 2, minor: 8 }), - )); + definitions.add(ContextSpecDefinition::new(Version { major: 0, minor: 1 })); definitions }; } diff --git a/apollo-federation/src/link/cost_spec_definition.rs b/apollo-federation/src/link/cost_spec_definition.rs index bd0894c392..66a5e91aa9 100644 --- a/apollo-federation/src/link/cost_spec_definition.rs +++ b/apollo-federation/src/link/cost_spec_definition.rs @@ -37,7 +37,6 @@ const LIST_SIZE_DIRECTIVE_REQUIRE_ONE_SLICING_ARGUMENT_ARGUMENT_NAME: Name = #[derive(Clone)] pub struct CostSpecDefinition { url: Url, - minimum_federation_version: Option, } macro_rules! propagate_demand_control_directives { @@ -109,13 +108,12 @@ macro_rules! propagate_demand_control_directives_to_position { } impl CostSpecDefinition { - pub(crate) fn new(version: Version, minimum_federation_version: Option) -> Self { + pub(crate) fn new(version: Version) -> Self { Self { url: Url { identity: Identity::cost_identity(), version, }, - minimum_federation_version, } } @@ -242,19 +240,12 @@ impl SpecDefinition for CostSpecDefinition { fn url(&self) -> &Url { &self.url } - - fn minimum_federation_version(&self) -> Option<&Version> { - self.minimum_federation_version.as_ref() - } } lazy_static! { pub(crate) static ref COST_VERSIONS: SpecDefinitions = { let mut definitions = SpecDefinitions::new(Identity::cost_identity()); - definitions.add(CostSpecDefinition::new( - Version { major: 0, minor: 1 }, - Some(Version { major: 2, minor: 9 }), - )); + definitions.add(CostSpecDefinition::new(Version { major: 0, minor: 1 })); definitions }; } diff --git a/apollo-federation/src/link/federation_spec_definition.rs b/apollo-federation/src/link/federation_spec_definition.rs index e89ce9f7ff..7f5e041aaf 100644 --- a/apollo-federation/src/link/federation_spec_definition.rs +++ b/apollo-federation/src/link/federation_spec_definition.rs @@ -543,10 +543,6 @@ impl SpecDefinition for FederationSpecDefinition { fn url(&self) -> &Url { &self.url } - - fn minimum_federation_version(&self) -> Option<&Version> { - None - } } lazy_static! { diff --git a/apollo-federation/src/link/inaccessible_spec_definition.rs b/apollo-federation/src/link/inaccessible_spec_definition.rs index fe72ba8c57..295fb3d7b3 100644 --- a/apollo-federation/src/link/inaccessible_spec_definition.rs +++ b/apollo-federation/src/link/inaccessible_spec_definition.rs @@ -39,17 +39,15 @@ pub(crate) const INACCESSIBLE_DIRECTIVE_NAME_IN_SPEC: Name = name!("inaccessible pub(crate) struct InaccessibleSpecDefinition { url: Url, - minimum_federation_version: Option, } impl InaccessibleSpecDefinition { - pub(crate) fn new(version: Version, minimum_federation_version: Option) -> Self { + pub(crate) fn new(version: Version) -> Self { Self { url: Url { identity: Identity::inaccessible_identity(), version, }, - minimum_federation_version, } } @@ -97,23 +95,19 @@ impl SpecDefinition for InaccessibleSpecDefinition { fn url(&self) -> &Url { &self.url } - - fn minimum_federation_version(&self) -> Option<&Version> { - self.minimum_federation_version.as_ref() - } } lazy_static! { pub(crate) static ref INACCESSIBLE_VERSIONS: SpecDefinitions = { let mut definitions = SpecDefinitions::new(Identity::inaccessible_identity()); - definitions.add(InaccessibleSpecDefinition::new( - Version { major: 0, minor: 1 }, - None, - )); - definitions.add(InaccessibleSpecDefinition::new( - Version { major: 0, minor: 2 }, - Some(Version { major: 2, minor: 0 }), - )); + definitions.add(InaccessibleSpecDefinition::new(Version { + major: 0, + minor: 1, + })); + definitions.add(InaccessibleSpecDefinition::new(Version { + major: 0, + minor: 2, + })); definitions }; } diff --git a/apollo-federation/src/link/join_spec_definition.rs b/apollo-federation/src/link/join_spec_definition.rs index 1bffc68b74..31f98eedf1 100644 --- a/apollo-federation/src/link/join_spec_definition.rs +++ b/apollo-federation/src/link/join_spec_definition.rs @@ -167,17 +167,15 @@ pub(crate) struct EnumValueDirectiveArguments { #[derive(Clone)] pub(crate) struct JoinSpecDefinition { url: Url, - minimum_federation_version: Option, } impl JoinSpecDefinition { - pub(crate) fn new(version: Version, minimum_federation_version: Option) -> Self { + pub(crate) fn new(version: Version) -> Self { Self { url: Url { identity: Identity::join_identity(), version, }, - minimum_federation_version, } } @@ -410,35 +408,16 @@ impl SpecDefinition for JoinSpecDefinition { fn url(&self) -> &Url { &self.url } - - fn minimum_federation_version(&self) -> Option<&Version> { - self.minimum_federation_version.as_ref() - } } lazy_static! { pub(crate) static ref JOIN_VERSIONS: SpecDefinitions = { let mut definitions = SpecDefinitions::new(Identity::join_identity()); - definitions.add(JoinSpecDefinition::new( - Version { major: 0, minor: 1 }, - None, - )); - definitions.add(JoinSpecDefinition::new( - Version { major: 0, minor: 2 }, - None, - )); - definitions.add(JoinSpecDefinition::new( - Version { major: 0, minor: 3 }, - Some(Version { major: 2, minor: 0 }), - )); - definitions.add(JoinSpecDefinition::new( - Version { major: 0, minor: 4 }, - Some(Version { major: 2, minor: 7 }), - )); - definitions.add(JoinSpecDefinition::new( - Version { major: 0, minor: 5 }, - Some(Version { major: 2, minor: 8 }), - )); + definitions.add(JoinSpecDefinition::new(Version { major: 0, minor: 1 })); + definitions.add(JoinSpecDefinition::new(Version { major: 0, minor: 2 })); + definitions.add(JoinSpecDefinition::new(Version { major: 0, minor: 3 })); + definitions.add(JoinSpecDefinition::new(Version { major: 0, minor: 4 })); + definitions.add(JoinSpecDefinition::new(Version { major: 0, minor: 5 })); definitions }; } diff --git a/apollo-federation/src/link/link_spec_definition.rs b/apollo-federation/src/link/link_spec_definition.rs index f1ed4035f7..59e75e42e1 100644 --- a/apollo-federation/src/link/link_spec_definition.rs +++ b/apollo-federation/src/link/link_spec_definition.rs @@ -8,18 +8,12 @@ use crate::link::spec_definition::SpecDefinitions; pub(crate) struct LinkSpecDefinition { url: Url, - minimum_federation_version: Option, } impl LinkSpecDefinition { - pub(crate) fn new( - version: Version, - minimum_federation_version: Option, - identity: Identity, - ) -> Self { + pub(crate) fn new(version: Version, identity: Identity) -> Self { Self { url: Url { identity, version }, - minimum_federation_version, } } } @@ -28,10 +22,6 @@ impl SpecDefinition for LinkSpecDefinition { fn url(&self) -> &Url { &self.url } - - fn minimum_federation_version(&self) -> Option<&Version> { - self.minimum_federation_version.as_ref() - } } lazy_static! { @@ -39,12 +29,10 @@ lazy_static! { let mut definitions = SpecDefinitions::new(Identity::core_identity()); definitions.add(LinkSpecDefinition::new( Version { major: 0, minor: 1 }, - None, Identity::core_identity(), )); definitions.add(LinkSpecDefinition::new( Version { major: 0, minor: 2 }, - Some(Version { major: 2, minor: 0 }), Identity::core_identity(), )); definitions @@ -53,7 +41,6 @@ lazy_static! { let mut definitions = SpecDefinitions::new(Identity::link_identity()); definitions.add(LinkSpecDefinition::new( Version { major: 1, minor: 0 }, - Some(Version { major: 2, minor: 0 }), Identity::link_identity(), )); definitions diff --git a/apollo-federation/src/link/mod.rs b/apollo-federation/src/link/mod.rs index 0b883fb36d..122a9a4d63 100644 --- a/apollo-federation/src/link/mod.rs +++ b/apollo-federation/src/link/mod.rs @@ -231,7 +231,7 @@ struct DisplayName<'s> { is_directive: bool, } -impl<'s> fmt::Display for DisplayName<'s> { +impl fmt::Display for DisplayName<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_directive { f.write_str("@")?; @@ -440,11 +440,11 @@ impl LinksMetadata { } pub fn all_links(&self) -> &[Arc] { - return self.links.as_ref(); + self.links.as_ref() } pub fn for_identity(&self, identity: &Identity) -> Option> { - return self.by_identity.get(identity).cloned(); + self.by_identity.get(identity).cloned() } pub fn source_link_of_type(&self, type_name: &Name) -> Option { diff --git a/apollo-federation/src/link/spec_definition.rs b/apollo-federation/src/link/spec_definition.rs index 5826f8f4d9..cb67081976 100644 --- a/apollo-federation/src/link/spec_definition.rs +++ b/apollo-federation/src/link/spec_definition.rs @@ -17,7 +17,6 @@ use crate::schema::FederationSchema; pub(crate) trait SpecDefinition { fn url(&self) -> &Url; - fn minimum_federation_version(&self) -> Option<&Version>; fn identity(&self) -> &Identity { &self.url().identity @@ -27,23 +26,6 @@ pub(crate) trait SpecDefinition { &self.url().version } - fn is_spec_directive_name( - &self, - schema: &FederationSchema, - name_in_schema: &Name, - ) -> Result { - let Some(metadata) = schema.metadata() else { - return Err(SingleFederationError::Internal { - message: "Schema is not a core schema (add @link first)".to_owned(), - } - .into()); - }; - Ok(metadata - .source_link_of_directive(name_in_schema) - .map(|e| e.link.url.identity == *self.identity()) - .unwrap_or(false)) - } - fn is_spec_type_name( &self, schema: &FederationSchema, diff --git a/apollo-federation/src/merge.rs b/apollo-federation/src/merge.rs index 231fb973b0..3ebfbda997 100644 --- a/apollo-federation/src/merge.rs +++ b/apollo-federation/src/merge.rs @@ -85,7 +85,7 @@ impl From for MergeFailure { } pub struct MergeFailure { - pub schema: Option, + pub schema: Option>, pub errors: Vec, pub composition_hints: Vec, } @@ -252,7 +252,7 @@ impl Merger { }) } else { Err(MergeFailure { - schema: Some(supergraph), + schema: Some(Box::new(supergraph)), composition_hints: self.composition_hints.to_owned(), errors: self.errors.to_owned(), }) @@ -1535,7 +1535,7 @@ fn join_graph_enum_type( fn add_core_feature_inaccessible(supergraph: &mut Schema) { // @link(url: "https://specs.apollo.dev/inaccessible/v0.2") - let spec = InaccessibleSpecDefinition::new(Version { major: 0, minor: 2 }, None); + let spec = InaccessibleSpecDefinition::new(Version { major: 0, minor: 2 }); supergraph .schema_definition diff --git a/apollo-federation/src/operation/directive_list.rs b/apollo-federation/src/operation/directive_list.rs index bd2f021f1a..cd55e0de71 100644 --- a/apollo-federation/src/operation/directive_list.rs +++ b/apollo-federation/src/operation/directive_list.rs @@ -304,9 +304,7 @@ impl DirectiveList { // Nothing to do on an empty list return None; }; - let Some(index) = inner.directives.iter().position(|dir| dir.name == name) else { - return None; - }; + let index = inner.directives.iter().position(|dir| dir.name == name)?; // The directive exists and is the only directive: switch to the empty representation if inner.len() == 1 { diff --git a/apollo-federation/src/operation/merging.rs b/apollo-federation/src/operation/merging.rs index 79ebec9fe3..adc6042bbb 100644 --- a/apollo-federation/src/operation/merging.rs +++ b/apollo-federation/src/operation/merging.rs @@ -20,7 +20,7 @@ use crate::ensure; use crate::error::FederationError; use crate::error::SingleFederationError; -impl<'a> FieldSelectionValue<'a> { +impl FieldSelectionValue<'_> { /// Merges the given field selections into this one. /// /// # Preconditions @@ -82,7 +82,7 @@ impl<'a> FieldSelectionValue<'a> { } } -impl<'a> InlineFragmentSelectionValue<'a> { +impl InlineFragmentSelectionValue<'_> { /// Merges the given normalized inline fragment selections into this one. /// /// # Preconditions @@ -117,7 +117,7 @@ impl<'a> InlineFragmentSelectionValue<'a> { } } -impl<'a> FragmentSpreadSelectionValue<'a> { +impl FragmentSpreadSelectionValue<'_> { /// Merges the given normalized fragment spread selections into this one. /// /// # Preconditions diff --git a/apollo-federation/src/operation/mod.rs b/apollo-federation/src/operation/mod.rs index 9454842f1d..6694642b75 100644 --- a/apollo-federation/src/operation/mod.rs +++ b/apollo-federation/src/operation/mod.rs @@ -1656,7 +1656,7 @@ impl SelectionSet { /// This tries to be smart about including or excluding the whole selection set. /// - If all selections have the same condition, returns that condition. /// - If selections in the set have different conditions, the selection set must always be - /// included, so the individual selections can be evaluated. + /// included, so the individual selections can be evaluated. /// /// # Errors /// Returns an error if the selection set contains a fragment spread, or if any of the @@ -1859,9 +1859,9 @@ impl SelectionSet { /// /// __typename is added to the sub selection set of a given selection in following conditions /// * if a given selection is a field, we add a __typename sub selection if its selection set type - /// position is an abstract type + /// position is an abstract type /// * if a given selection is a fragment, we only add __typename sub selection if fragment specifies - /// type condition and that type condition is an abstract type. + /// type condition and that type condition is an abstract type. pub(crate) fn add_typename_field_for_abstract_types( &self, parent_type_if_abstract: Option, diff --git a/apollo-federation/src/operation/optimize.rs b/apollo-federation/src/operation/optimize.rs index 7bdd0842a2..2eaa2ed47c 100644 --- a/apollo-federation/src/operation/optimize.rs +++ b/apollo-federation/src/operation/optimize.rs @@ -40,7 +40,6 @@ use std::sync::Arc; use apollo_compiler::collections::IndexMap; use apollo_compiler::executable; use apollo_compiler::Name; -use apollo_compiler::Node; use super::Fragment; use super::FragmentSpreadSelection; @@ -108,16 +107,6 @@ impl SelectionSet { } } -//============================================================================= -// Matching fragments with selection set (`try_optimize_with_fragments`) - -/// The return type for `SelectionSet::try_optimize_with_fragments`. -#[derive(derive_more::From)] -enum SelectionSetOrFragment { - SelectionSet(SelectionSet), - Fragment(Node), -} - // Note: `retain_fragments` methods may return a selection or a selection set. impl From for SelectionMapperReturn { fn from(value: SelectionOrSet) -> Self { @@ -392,7 +381,6 @@ mod tests { /// /// empty branches removal /// - mod test_empty_branch_removal { use apollo_compiler::name; diff --git a/apollo-federation/src/operation/selection_map.rs b/apollo-federation/src/operation/selection_map.rs index 477e2548ef..418b9562eb 100644 --- a/apollo-federation/src/operation/selection_map.rs +++ b/apollo-federation/src/operation/selection_map.rs @@ -138,12 +138,12 @@ impl OwnedSelectionKey { } } +#[cfg(test)] impl<'a> SelectionKey<'a> { /// Create a selection key for a specific field name. /// /// This is available for tests only as selection keys should not normally be created outside of /// `HasSelectionKey::key`. - #[cfg(test)] pub(crate) fn field_name(name: &'a Name) -> Self { static EMPTY_LIST: DirectiveList = DirectiveList::new(); SelectionKey::Field { diff --git a/apollo-federation/src/operation/simplify.rs b/apollo-federation/src/operation/simplify.rs index 29d4c55d9f..caaa313430 100644 --- a/apollo-federation/src/operation/simplify.rs +++ b/apollo-federation/src/operation/simplify.rs @@ -551,8 +551,7 @@ type Query { // Use apollo-compiler's selection set printer directly instead of the minimized // apollo-federation printer - let compiler_set = - apollo_compiler::executable::SelectionSet::try_from(&expanded_and_flattened).unwrap(); + let compiler_set = executable::SelectionSet::try_from(&expanded_and_flattened).unwrap(); insta::assert_snapshot!(compiler_set, @r#" { diff --git a/apollo-federation/src/operation/tests/mod.rs b/apollo-federation/src/operation/tests/mod.rs index b0329cbb3d..0b7bda745d 100644 --- a/apollo-federation/src/operation/tests/mod.rs +++ b/apollo-federation/src/operation/tests/mod.rs @@ -51,11 +51,7 @@ pub(super) fn parse_and_expand( schema: &ValidFederationSchema, query: &str, ) -> Result { - let doc = apollo_compiler::ExecutableDocument::parse_and_validate( - schema.schema(), - query, - "query.graphql", - )?; + let doc = ExecutableDocument::parse_and_validate(schema.schema(), query, "query.graphql")?; let operation = doc .operations @@ -1055,7 +1051,7 @@ scalar FieldSet /// https://github.com/apollographql/federation-next/pull/290#discussion_r1587200664 #[test] fn converting_operation_types() { - let schema = apollo_compiler::Schema::parse_and_validate( + let schema = Schema::parse_and_validate( r#" interface Intf { intfField: Int @@ -1389,9 +1385,7 @@ const ADD_AT_PATH_TEST_SCHEMA: &str = r#" #[test] fn add_at_path_merge_scalar_fields() { - let schema = - apollo_compiler::Schema::parse_and_validate(ADD_AT_PATH_TEST_SCHEMA, "schema.graphql") - .unwrap(); + let schema = Schema::parse_and_validate(ADD_AT_PATH_TEST_SCHEMA, "schema.graphql").unwrap(); let schema = ValidFederationSchema::new(schema).unwrap(); let mut selection_set = SelectionSet::empty( @@ -1418,9 +1412,7 @@ fn add_at_path_merge_scalar_fields() { #[test] fn add_at_path_merge_subselections() { - let schema = - apollo_compiler::Schema::parse_and_validate(ADD_AT_PATH_TEST_SCHEMA, "schema.graphql") - .unwrap(); + let schema = Schema::parse_and_validate(ADD_AT_PATH_TEST_SCHEMA, "schema.graphql").unwrap(); let schema = ValidFederationSchema::new(schema).unwrap(); let mut selection_set = SelectionSet::empty( @@ -1468,9 +1460,7 @@ fn add_at_path_merge_subselections() { #[test] fn add_at_path_collapses_unnecessary_fragments() { - let schema = - apollo_compiler::Schema::parse_and_validate(ADD_AT_PATH_TEST_SCHEMA, "schema.graphql") - .unwrap(); + let schema = Schema::parse_and_validate(ADD_AT_PATH_TEST_SCHEMA, "schema.graphql").unwrap(); let schema = ValidFederationSchema::new(schema).unwrap(); let mut selection_set = SelectionSet::empty( diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index df480aba81..6078d06c3b 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -881,10 +881,7 @@ where // multiple maximum items. // Note: `position_max` returns the last of the equally maximum items. Thus, we use // `position_min_by` by reversing the ordering. - let pos = self.items.iter().position_min_by(|a, b| b.cmp(a)); - let Some(pos) = pos else { - return None; - }; + let pos = self.items.iter().position_min_by(|a, b| b.cmp(a))?; Some(self.items.remove(pos)) } } @@ -924,7 +921,8 @@ pub(crate) enum GraphPathTriggerRef<'a> { #[derive(derive_more::From)] pub(crate) enum GraphPathTriggerRefMut<'a> { Op(&'a mut OpGraphPathTrigger), - Transition(&'a mut QueryGraphEdgeTransition), + // Unused: + // Transition(&'a mut QueryGraphEdgeTransition), } impl<'a> From<&'a GraphPathTrigger> for GraphPathTriggerRef<'a> { @@ -941,7 +939,7 @@ impl<'a> From<&'a GraphPathTrigger> for GraphPathTriggerRef<'a> { /// the `GraphPathTrigger`. Rather than trying to cast into concrete types and cast back (and /// potentially raise errors), this trait provides ways to access the data needed within. pub(crate) trait GraphPathTriggerVariant: Eq + Hash + std::fmt::Debug { - fn get_field_mut<'a>(&'a mut self) -> Option<&mut Field> + fn get_field_mut<'a>(&'a mut self) -> Option<&'a mut Field> where &'a mut Self: Into>, { @@ -2760,10 +2758,10 @@ impl OpGraphPath { // So far, so good. Check that the rest of the paths are equal. Note that starts with // `diff_pos + 1` for `self`, but `diff_pos + 2` for `other` since we looked at two edges // there instead of one. - return Ok(self.edges[(diff_pos + 1)..] + Ok(self.edges[(diff_pos + 1)..] .iter() .zip(other.edges[(diff_pos + 2)..].iter()) - .all(|(self_edge, other_edge)| self_edge == other_edge)); + .all(|(self_edge, other_edge)| self_edge == other_edge)) } /// This method is used to detect when using an interface field "directly" could fail (i.e. lead @@ -4353,7 +4351,7 @@ fn is_useless_followup_element( // The followup is useless if it's a fragment (with no directives we would want to preserve) whose type // is already that of the first element (or a supertype). - return match followup { + match followup { OpPathElement::Field(_) => Ok(false), OpPathElement::InlineFragment(fragment) => { let Some(type_of_second) = fragment.type_condition_position.clone() else { @@ -4369,7 +4367,7 @@ fn is_useless_followup_element( .is_subtype(type_of_second.type_name(), type_of_first.type_name()); Ok(are_useless_directives && (is_same_type || is_subtype)) } - }; + } } #[cfg(test)] diff --git a/apollo-federation/src/query_graph/mod.rs b/apollo-federation/src/query_graph/mod.rs index 07f8ceeaff..b37f4bb4fc 100644 --- a/apollo-federation/src/query_graph/mod.rs +++ b/apollo-federation/src/query_graph/mod.rs @@ -855,7 +855,7 @@ impl QueryGraph { "Unexpectedly encountered federation root node as tail node.", )); }; - return match &edge_weight.transition { + match &edge_weight.transition { QueryGraphEdgeTransition::FieldCollection { source, field_definition_position, @@ -920,7 +920,7 @@ impl QueryGraph { QueryGraphEdgeTransition::InterfaceObjectFakeDownCast { .. } => { Ok(possible_runtime_types.clone()) } - }; + } } /// Returns a selection set that can be used as a key for the given type, and that can be diff --git a/apollo-federation/src/query_graph/path_tree.rs b/apollo-federation/src/query_graph/path_tree.rs index 8f3c828e98..b8564f90ed 100644 --- a/apollo-federation/src/query_graph/path_tree.rs +++ b/apollo-federation/src/query_graph/path_tree.rs @@ -246,6 +246,7 @@ where /// trigger: the final trigger value /// - Two equivalent triggers can have minor differences in the sibling_typename. /// This field holds the final trigger value that will be used. + /// /// PORT_NOTE: The JS QP used the last trigger value. So, we are following that /// to avoid mismatches. But, it can be revisited. /// We may want to keep or merge the sibling_typename values. diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index 7c92af6fde..21455307df 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -14,7 +14,6 @@ use apollo_compiler::collections::IndexSet; use apollo_compiler::executable; use apollo_compiler::executable::VariableDefinition; use apollo_compiler::name; -use apollo_compiler::schema; use apollo_compiler::Name; use apollo_compiler::Node; use itertools::Itertools; @@ -111,7 +110,7 @@ impl DeferredNodes { fn iter(&self) -> impl Iterator)> { self.inner .iter() - .flat_map(|(defer_ref, nodes)| std::iter::repeat(defer_ref).zip(nodes.iter().copied())) + .flat_map(|(defer_ref, nodes)| iter::repeat(defer_ref).zip(nodes.iter().copied())) } /// Consume the map and yield each element. This is provided as a standalone method and not an @@ -120,7 +119,7 @@ impl DeferredNodes { self.inner.into_iter().flat_map(|(defer_ref, nodes)| { // Cloning the key is a bit wasteful, but keys are typically very small, // and this map is also very small. - std::iter::repeat_with(move || defer_ref.clone()).zip(nodes) + iter::repeat_with(move || defer_ref.clone()).zip(nodes) }) } } @@ -670,8 +669,8 @@ impl FetchDependencyGraphNodePath { let mut type_ = &field.field_position.get(field.schema.schema())?.ty; loop { match type_ { - schema::Type::Named(_) | schema::Type::NonNullNamed(_) => break, - schema::Type::List(inner) | schema::Type::NonNullList(inner) => { + Type::Named(_) | Type::NonNullNamed(_) => break, + Type::List(inner) | Type::NonNullList(inner) => { new_path.push(FetchDataPathElement::AnyIndex(Default::default())); type_ = inner } @@ -1807,7 +1806,7 @@ impl FetchDependencyGraph { )?; let reduced_sequence = - processor.reduce_sequence(std::iter::once(processed).chain(main_sequence)); + processor.reduce_sequence(iter::once(processed).chain(main_sequence)); Ok(( processor.on_conditions(&conditions, reduced_sequence), all_deferred_nodes, @@ -3035,7 +3034,7 @@ fn operation_for_entities_fetch( })?; let query_type = match subgraph_schema.get_type(query_type_name.clone())? { - crate::schema::position::TypeDefinitionPosition::Object(o) => o, + TypeDefinitionPosition::Object(o) => o, _ => { return Err(SingleFederationError::InvalidSubgraph { message: "the root query type must be an object".to_string(), diff --git a/apollo-federation/src/query_plan/generate.rs b/apollo-federation/src/query_plan/generate.rs index 4001b70f6f..30cc36920c 100644 --- a/apollo-federation/src/query_plan/generate.rs +++ b/apollo-federation/src/query_plan/generate.rs @@ -251,7 +251,7 @@ mod tests { target_len: usize, } - impl<'a> PlanBuilder for TestPlanBuilder<'a> { + impl PlanBuilder for TestPlanBuilder<'_> { fn add_to_plan( &mut self, partial_plan: &Plan, diff --git a/apollo-federation/src/query_plan/query_planning_traversal.rs b/apollo-federation/src/query_plan/query_planning_traversal.rs index 1739fc21cc..90f4ecc756 100644 --- a/apollo-federation/src/query_plan/query_planning_traversal.rs +++ b/apollo-federation/src/query_plan/query_planning_traversal.rs @@ -371,7 +371,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { } } self.compute_best_plan_from_closed_branches()?; - return Ok(self.best_plan.as_ref()); + Ok(self.best_plan.as_ref()) } /// Returns whether to terminate planning immediately, and any new open branches to push onto @@ -1201,7 +1201,7 @@ impl<'a: 'b, 'b> PlanBuilder> for QueryPlanningTravers // The same would be infeasible to implement in Rust due to the cyclic references. // Thus, instead of `condition_resolver` field, QueryPlanningTraversal was made to // implement `ConditionResolver` trait along with `resolver_cache` field. -impl<'a> ConditionResolver for QueryPlanningTraversal<'a, '_> { +impl ConditionResolver for QueryPlanningTraversal<'_, '_> { /// A query plan resolver for edge conditions that caches the outcome per edge. #[track_caller] fn resolve( diff --git a/apollo-federation/src/schema/mod.rs b/apollo-federation/src/schema/mod.rs index 3def48068b..2cc1f233cb 100644 --- a/apollo-federation/src/schema/mod.rs +++ b/apollo-federation/src/schema/mod.rs @@ -165,6 +165,7 @@ impl FederationSchema { /// Similar to `Self::validate` but returns `self` as part of the error should it be needed by /// the caller + #[allow(clippy::result_large_err)] // lint is accurate but this is not in a hot path pub(crate) fn validate_or_return_self( mut self, ) -> Result { @@ -229,6 +230,7 @@ impl ValidFederationSchema { } /// Construct a ValidFederationSchema by assuming the given FederationSchema is valid. + #[allow(clippy::result_large_err)] // lint is accurate but this is not in a hot path fn new_assume_valid( mut schema: FederationSchema, ) -> Result { diff --git a/apollo-federation/src/schema/type_and_directive_specification.rs b/apollo-federation/src/schema/type_and_directive_specification.rs index 3396c8e0d9..e9f426b0f3 100644 --- a/apollo-federation/src/schema/type_and_directive_specification.rs +++ b/apollo-federation/src/schema/type_and_directive_specification.rs @@ -810,7 +810,6 @@ mod tests { fn link_spec(_version: Version) -> Box { Box::new(LinkSpecDefinition::new( Version { major: 1, minor: 0 }, - None, Identity { domain: String::from("https://specs.apollo.dev/link/v1.0"), name: name!("link"), @@ -859,7 +858,6 @@ mod tests { fn link_spec(_version: Version) -> Box { Box::new(LinkSpecDefinition::new( Version { major: 1, minor: 0 }, - None, Identity { domain: String::from("https://specs.apollo.dev/link/v1.0"), name: name!("link"), diff --git a/apollo-federation/src/supergraph/mod.rs b/apollo-federation/src/supergraph/mod.rs index d6d05a5d68..d4912ad4d7 100644 --- a/apollo-federation/src/supergraph/mod.rs +++ b/apollo-federation/src/supergraph/mod.rs @@ -2112,7 +2112,6 @@ fn maybe_dump_subgraph_schema(subgraph: FederationSubgraph, message: &mut String //////////////////////////////////////////////////////////////////////////////// /// @join__directive extraction - static JOIN_DIRECTIVE: &str = "join__directive"; /// Converts `@join__directive(graphs: [A], name: "foo")` to `@foo` in the A subgraph. diff --git a/apollo-federation/src/utils/fallible_iterator.rs b/apollo-federation/src/utils/fallible_iterator.rs index d8e3065249..df0da01841 100644 --- a/apollo-federation/src/utils/fallible_iterator.rs +++ b/apollo-federation/src/utils/fallible_iterator.rs @@ -2,25 +2,6 @@ use itertools::Itertools; -/// A common use for iteator is to collect into a container and grow that container. This trait -/// extends the standard library's `Extend` trait to work for containers that can be extended with -/// `T`s to also be extendable with `Result`. If an `Err` is encountered, that `Err` is -/// returned. Notably, this means the container will contain all prior `Ok` values. -pub(crate) trait FallibleExtend: Extend { - fn fallible_extend(&mut self, iter: I) -> Result<(), E> - where - I: IntoIterator>, - { - iter.into_iter() - .process_results(|results| self.extend(results)) - } - - // NOTE: The standard extend trait provides `extend_one` and `extend_reserve` methods. These - // have not been added and can be if a use arises. -} - -impl FallibleExtend for T where T: Extend {} - /// An extension trait for `Iterator`, similar to `Itertools`, that seeks to improve the ergonomics /// around fallible operations. /// @@ -29,11 +10,11 @@ impl FallibleExtend for T where T: Extend {} /// also refer to the stream of items that you're iterating over (or both!). As much as possible, I /// will use the following naming scheme in order to keep these ideas consistent: /// - If the iterator yeilds an arbitary `T` and the operation that you wish to apply is of the -/// form `T -> Result`, then it will named `fallible_*`. +/// form `T -> Result`, then it will named `fallible_*`. /// - If the iterator yields `Result` and the operation is of the form `T -> U` (for arbitary -/// `U`), then it will named `*_ok`. +/// `U`), then it will named `*_ok`. /// - If both iterator and operation yield `Result`, then it will named `and_then_*` (more on that -/// fewer down). +/// fewer down). /// /// The first category mostly describes combinators that take closures that need specific types, /// such as `filter` and things in the `any`/`all`/`find`/`fold` family. There are several @@ -173,82 +154,6 @@ pub(crate) trait FallibleIterator: Sized + Itertools { Ok(digest) } - /// This method functions similarly to `FallibleIterator::fallible_all` but inverted. The - /// existing iterator yields `Result`s but the predicate is not fallible. - /// - /// Like `FallibleIterator::fallible_all`, this function short-curcuits but will short-curcuit - /// if it encounters an `Err` or `false`. If the existing iterator yields an `Err`, this - /// function short-curcuits, does not call the predicate, and returns that `Err`. If the value - /// is `Ok`, it is given to the predicate. If the predicate returns `false`, this method - /// returns `Ok(false)`. - /// - /// ```ignore - /// let first_values = vec![]; - /// let second_values = vec![Ok(1), Err(())]; - /// let third_values = vec![Ok(0), Ok(1), Ok(2)]; - /// let fourth_values = vec![Err(()), Ok(0)]; - /// - /// assert_eq!(Ok(true), first_values.into_iter().ok_and_all(is_even)); - /// assert_eq!(Ok(false), second_values.into_iter().ok_and_all(is_even)); - /// assert_eq!(Ok(false), third_values.into_iter().ok_and_all(is_even)); - /// assert_eq!(Err(()), fourth_values.into_iter().ok_and_all(is_even)); - /// ``` - fn ok_and_all(&mut self, predicate: F) -> Result - where - Self: Iterator>, - F: FnMut(T) -> bool, - { - self.process_results(|mut results| results.all(predicate)) - } - - /// This method functions similarly to `FallibleIterator::fallible_all` but both the - /// existing iterator and predicate yield `Result`s. - /// - /// Like `FallibleIterator::fallible_all`, this function short-curcuits but will short-curcuit - /// if it encounters an `Err` or `Ok(false)`. If the existing iterator yields an `Err`, this - /// function returns that `Err`. If the value is `Ok`, it is given to the predicate. If the - /// predicate returns `Err`, that `Err` is returned. If the predicate returns `Ok(false)`, - /// `Ok(false)` is returned. By default, this function returned `Ok(true)`. - /// - /// ```ignore - /// // A totally accurate prime checker - /// fn is_prime(i: usize) -> Result { - /// match i { - /// 0 | 1 => Err(()), // 0 and 1 are neither prime or composite - /// 2 | 3 => Ok(true), - /// _ => Ok(false), // Every other number is composite, I guess - /// } - /// } - /// - /// let first_values = vec![]; - /// let second_values = vec![Ok(0), Err(())]; - /// let third_values = vec![Ok(2), Ok(3)]; - /// let fourth_values = vec![Err(()), Ok(2)]; - /// let fifth_values = vec![Ok(2), Err(())]; - /// let sixth_values = vec![Ok(4), Ok(3)]; - /// - /// assert_eq!(Ok(true), first_values.into_iter().and_then_all(is_prime)); - /// assert_eq!(Err(()), second_values.into_iter().and_then_all(is_prime)); - /// assert_eq!(Ok(true), third_values.into_iter().and_then_all(is_prime)); - /// assert_eq!(Err(()), fourth_values.into_iter().and_then_all(is_prime)); - /// assert_eq!(Err(()), fifth_values.into_iter().and_then_all(is_prime)); - /// assert_eq!(Ok(false), sixth_values.into_iter().and_then_all(is_prime)); - /// ``` - fn and_then_all(&mut self, mut predicate: F) -> Result - where - Self: Iterator>, - F: FnMut(T) -> Result, - { - let mut digest = true; - for val in self.by_ref() { - digest &= val.and_then(&mut predicate)?; - if !digest { - break; - } - } - Ok(digest) - } - /// This method functions similarly to `Iterator::any` but where the given predicate returns /// `Result`s. /// @@ -315,54 +220,6 @@ pub(crate) trait FallibleIterator: Sized + Itertools { self.process_results(|mut results| results.any(predicate)) } - /// This method functions similarly to `FallibleIterator::fallible_any` but both the - /// existing iterator and predicate yield `Result`s. - /// - /// Like `FallibleIterator::fallible_any`, this function short-curcuits but will short-curcuit - /// if it encounters an `Err` or `Ok(true)`. If the existing iterator yields an `Err`, this - /// function returns that `Err`. If the value is `Ok`, it is given to the predicate. If the - /// predicate returns `Err`, that `Err` is returned. If the predicate returns `Ok(true)`, - /// `Ok(true)` is returned. By default, this function returned `Ok(false)`. - /// - /// ```ignore - /// // A totally accurate prime checker - /// fn is_prime(i: usize) -> Result { - /// match i { - /// 0 | 1 => Err(()), // 0 and 1 are neither prime or composite - /// 2 | 3 => Ok(true), - /// _ => Ok(false), // Every other number is composite, I guess - /// } - /// } - /// - /// let first_values = vec![]; - /// let second_values = vec![Ok(0), Err(())]; - /// let third_values = vec![Ok(3), Ok(4)]; - /// let fourth_values = vec![Err(()), Ok(2)]; - /// let fifth_values = vec![Ok(2), Err(())]; - /// let sixth_values = vec![Ok(4), Ok(5)]; - /// - /// assert_eq!(Ok(false), first_values.into_iter().and_then_any(is_prime)); - /// assert_eq!(Err(()), second_values.into_iter().and_then_any(is_prime)); - /// assert_eq!(Ok(true), third_values.into_iter().and_then_any(is_prime)); - /// assert_eq!(Err(()), fourth_values.into_iter().and_then_any(is_prime)); - /// assert_eq!(Ok(true), fifth_values.into_iter().and_then_any(is_prime)); - /// assert_eq!(Ok(false), sixth_values.into_iter().and_then_any(is_prime)); - /// ``` - fn and_then_any(&mut self, mut predicate: F) -> Result - where - Self: Iterator>, - F: FnMut(T) -> Result, - { - let mut digest = false; - for val in self { - digest |= val.and_then(&mut predicate)?; - if digest { - break; - } - } - Ok(digest) - } - /// A convenience method that is equivalent to calling `.map(|result| /// result.and_then(fallible_fn))`. fn and_then(self, map: F) -> AndThen @@ -373,26 +230,6 @@ pub(crate) trait FallibleIterator: Sized + Itertools { AndThen { iter: self, map } } - /// A convenience method that is equivalent to calling `.map(|result| - /// result.or_else(fallible_fn))`. - fn or_else(self, map: F) -> OrElse - where - Self: Iterator>, - F: FnMut(E) -> Result, - { - OrElse { iter: self, map } - } - - /// A convenience method for applying a fallible operation to an iterator of `Result`s and - /// returning the first `Err` if one occurs. - fn and_then_for_each(self, inner: F) -> Result<(), E> - where - Self: Iterator>, - F: FnMut(T) -> Result<(), E>, - { - self.and_then(inner).collect() - } - /// Tries to find the first `Ok` value that matches the predicate. If an `Err` is found before /// the finding a match, the `Err` is returned. // NOTE: This is a nightly feature on `Iterator`. To avoid name collisions, this method is @@ -416,17 +253,6 @@ pub(crate) trait FallibleIterator: Sized + Itertools { } Ok(init) } - - fn and_then_fold(&mut self, mut init: O, mut mapper: F) -> Result - where - Self: Iterator>, - F: FnMut(O, T) -> Result, - { - for item in self { - init = mapper(init, item?)? - } - Ok(init) - } } impl FallibleIterator for I {} @@ -501,23 +327,6 @@ where } } -pub(crate) struct OrElse { - iter: I, - map: F, -} - -impl Iterator for OrElse -where - I: Iterator>, - F: FnMut(E) -> Result, -{ - type Item = Result; - - fn next(&mut self) -> Option { - self.iter.next().map(|res| res.or_else(&mut self.map)) - } -} - // Ideally, these would be doc tests, but gating access to the `utils` module is messy. #[cfg(test)] mod tests { @@ -560,36 +369,6 @@ mod tests { assert_eq!(Err(()), (1..5).fallible_all(is_prime)); } - #[test] - fn test_ok_and_all() { - let first_values: Vec = vec![]; - let second_values: Vec = vec![Ok(1), Err(())]; - let third_values: Vec = vec![Ok(0), Ok(1), Ok(2)]; - let fourth_values: Vec = vec![Err(()), Ok(0)]; - - assert_eq!(Ok(true), first_values.into_iter().ok_and_all(is_even)); - assert_eq!(Ok(false), second_values.into_iter().ok_and_all(is_even)); - assert_eq!(Ok(false), third_values.into_iter().ok_and_all(is_even)); - assert_eq!(Err(()), fourth_values.into_iter().ok_and_all(is_even)); - } - - #[test] - fn test_and_then_all() { - let first_values: Vec = vec![]; - let second_values: Vec = vec![Ok(0), Err(())]; - let third_values: Vec = vec![Ok(2), Ok(3)]; - let fourth_values: Vec = vec![Err(()), Ok(2)]; - let fifth_values: Vec = vec![Ok(2), Err(())]; - let sixth_values: Vec = vec![Ok(4), Ok(3)]; - - assert_eq!(Ok(true), first_values.into_iter().and_then_all(is_prime)); - assert_eq!(Err(()), second_values.into_iter().and_then_all(is_prime)); - assert_eq!(Ok(true), third_values.into_iter().and_then_all(is_prime)); - assert_eq!(Err(()), fourth_values.into_iter().and_then_all(is_prime)); - assert_eq!(Err(()), fifth_values.into_iter().and_then_all(is_prime)); - assert_eq!(Ok(false), sixth_values.into_iter().and_then_all(is_prime)); - } - #[test] fn test_fallible_any() { assert_eq!(Ok(false), [].into_iter().fallible_any(is_prime)); @@ -611,21 +390,4 @@ mod tests { assert_eq!(Ok(false), third_values.into_iter().ok_and_any(is_even)); assert_eq!(Err(()), fourth_values.into_iter().ok_and_any(is_even)); } - - #[test] - fn test_and_then_any() { - let first_values: Vec = vec![]; - let second_values: Vec = vec![Ok(0), Err(())]; - let third_values: Vec = vec![Ok(3), Ok(4)]; - let fourth_values: Vec = vec![Err(()), Ok(2)]; - let fifth_values: Vec = vec![Ok(2), Err(())]; - let sixth_values: Vec = vec![Ok(4), Ok(5)]; - - assert_eq!(Ok(false), first_values.into_iter().and_then_any(is_prime)); - assert_eq!(Err(()), second_values.into_iter().and_then_any(is_prime)); - assert_eq!(Ok(true), third_values.into_iter().and_then_any(is_prime)); - assert_eq!(Err(()), fourth_values.into_iter().and_then_any(is_prime)); - assert_eq!(Ok(true), fifth_values.into_iter().and_then_any(is_prime)); - assert_eq!(Ok(false), sixth_values.into_iter().and_then_any(is_prime)); - } } diff --git a/apollo-federation/src/utils/logging.rs b/apollo-federation/src/utils/logging.rs index 0b247c1698..9817dfd014 100644 --- a/apollo-federation/src/utils/logging.rs +++ b/apollo-federation/src/utils/logging.rs @@ -57,7 +57,7 @@ pub(crate) fn make_string( writer: fn(&mut std::fmt::Formatter<'_>, &T) -> std::fmt::Result, } - impl<'a, T: ?Sized> std::fmt::Display for Stringify<'a, T> { + impl std::fmt::Display for Stringify<'_, T> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { (self.writer)(f, self.data) } diff --git a/apollo-router-scaffold/scaffold-test/Dockerfile b/apollo-router-scaffold/scaffold-test/Dockerfile index 28e91cc9a6..3d3263130e 100644 --- a/apollo-router-scaffold/scaffold-test/Dockerfile +++ b/apollo-router-scaffold/scaffold-test/Dockerfile @@ -1,6 +1,6 @@ # Use the rust build image from docker as our base # renovate-automation: rustc version -FROM rust:1.76.0 as build +FROM rust:1.83.0 as build # Set our working directory for the build WORKDIR /usr/src/router diff --git a/apollo-router-scaffold/templates/base/Dockerfile b/apollo-router-scaffold/templates/base/Dockerfile index 28e91cc9a6..3d3263130e 100644 --- a/apollo-router-scaffold/templates/base/Dockerfile +++ b/apollo-router-scaffold/templates/base/Dockerfile @@ -1,6 +1,6 @@ # Use the rust build image from docker as our base # renovate-automation: rustc version -FROM rust:1.76.0 as build +FROM rust:1.83.0 as build # Set our working directory for the build WORKDIR /usr/src/router diff --git a/apollo-router-scaffold/templates/base/rust-toolchain.toml b/apollo-router-scaffold/templates/base/rust-toolchain.toml index 65542fa528..08f13cdb9f 100644 --- a/apollo-router-scaffold/templates/base/rust-toolchain.toml +++ b/apollo-router-scaffold/templates/base/rust-toolchain.toml @@ -3,4 +3,4 @@ [toolchain] # renovate-automation: rustc version channel = "1.76.0" -components = [ "rustfmt", "clippy" ] +components = ["rustfmt", "clippy"] diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 890d9feb25..3615b79d0a 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -150,8 +150,6 @@ opentelemetry-aws = "0.8.0" # opentelemetry-datadog = { version = "0.8.0", features = ["reqwest-client"] } rmp = "0.8" # END TEMP DATADOG -# Pin rowan until update to rust 1.77 -rowan = "=0.15.15" opentelemetry-http = "0.9.0" opentelemetry-jaeger = { version = "0.19.0", features = [ "collector_client", diff --git a/apollo-router/README.md b/apollo-router/README.md index 96a32e23f1..77f8068966 100644 --- a/apollo-router/README.md +++ b/apollo-router/README.md @@ -27,4 +27,4 @@ Most Apollo Router Core features can be defined using our [YAML configuration](h If you prefer to write customizations in Rust or need more advanced customizations, see our section on [native customizations](https://www.apollographql.com/docs/router/customizations/native) for information on how to use `apollo-router` as a Rust library. We also publish Rust-specific documentation on our [`apollo-router` crate docs](https://docs.rs/crate/apollo-router). -The minimum supported Rust version (MSRV) for this version of `apollo-router` is **1.76.0**. +The minimum supported Rust version (MSRV) for this version of `apollo-router` is **1.83.0**. diff --git a/apollo-router/src/ageing_priority_queue.rs b/apollo-router/src/ageing_priority_queue.rs index d206283093..a7dbb3fbc9 100644 --- a/apollo-router/src/ageing_priority_queue.rs +++ b/apollo-router/src/ageing_priority_queue.rs @@ -85,7 +85,7 @@ where } } -impl<'a, T> Receiver<'a, T> +impl Receiver<'_, T> where T: Send + 'static, { diff --git a/apollo-router/src/apollo_studio_interop/mod.rs b/apollo-router/src/apollo_studio_interop/mod.rs index b9877374ea..98c3f59484 100644 --- a/apollo-router/src/apollo_studio_interop/mod.rs +++ b/apollo-router/src/apollo_studio_interop/mod.rs @@ -763,7 +763,7 @@ struct SignatureFormatterWithAlgorithm<'a> { normalization_algorithm: &'a ApolloSignatureNormalizationAlgorithm, } -impl<'a> fmt::Display for SignatureFormatterWithAlgorithm<'a> { +impl fmt::Display for SignatureFormatterWithAlgorithm<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self.formatter { ApolloReportingSignatureFormatter::Operation(operation) => { 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 391424f0b1..95404382f4 100644 --- a/apollo-router/src/axum_factory/axum_http_server_factory.rs +++ b/apollo-router/src/axum_factory/axum_http_server_factory.rs @@ -746,7 +746,7 @@ impl<'a> CancelHandler<'a> { } } -impl<'a> Drop for CancelHandler<'a> { +impl Drop for CancelHandler<'_> { fn drop(&mut self) { if !self.got_first_response { if self.experimental_log_on_broken_pipe { diff --git a/apollo-router/src/batching.rs b/apollo-router/src/batching.rs index daa7884d99..0901497fdc 100644 --- a/apollo-router/src/batching.rs +++ b/apollo-router/src/batching.rs @@ -110,14 +110,16 @@ impl BatchQuery { .await .as_ref() .ok_or(SubgraphBatchingError::SenderUnavailable)? - .send(BatchHandlerMessage::Progress { - index: self.index, - client_factory, - request, - gql_request, - response_sender: tx, - span_context: Span::current().context(), - }) + .send(BatchHandlerMessage::Progress(Box::new( + BatchHandlerMessageProgress { + index: self.index, + client_factory, + request, + gql_request, + response_sender: tx, + span_context: Span::current().context(), + }, + ))) .await?; if !self.finished() { @@ -163,18 +165,13 @@ impl BatchQuery { // #[derive(Debug)] enum BatchHandlerMessage { /// Cancel one of the batch items - Cancel { index: usize, reason: String }, - - /// A query has reached the subgraph service and we should update its state - Progress { + Cancel { index: usize, - client_factory: HttpClientServiceFactory, - request: SubgraphRequest, - gql_request: graphql::Request, - response_sender: oneshot::Sender>, - span_context: otelContext, + reason: String, }, + Progress(Box), + /// A query has passed query planning and knows how many fetches are needed /// to complete. Begin { @@ -183,6 +180,16 @@ enum BatchHandlerMessage { }, } +/// A query has reached the subgraph service and we should update its state +struct BatchHandlerMessageProgress { + index: usize, + client_factory: HttpClientServiceFactory, + request: SubgraphRequest, + gql_request: graphql::Request, + response_sender: oneshot::Sender>, + span_context: otelContext, +} + /// Collection of info needed to resolve a batch query pub(crate) struct BatchQueryInfo { /// The owning subgraph request @@ -306,15 +313,16 @@ impl Batch { ); } - BatchHandlerMessage::Progress { - index, - client_factory, - request, - gql_request, - response_sender, - span_context, - } => { + BatchHandlerMessage::Progress(progress) => { // Progress the index + let BatchHandlerMessageProgress { + index, + client_factory, + request, + gql_request, + response_sender, + span_context, + } = *progress; tracing::debug!("Progress index: {index}"); diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index a201a647cb..3b83d9416a 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -273,6 +273,7 @@ fn test_listen() -> ListenAddr { #[buildstructor::buildstructor] impl Configuration { #[builder] + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder pub(crate) fn new( supergraph: Option, health_check: Option, @@ -395,6 +396,7 @@ impl Default for Configuration { #[buildstructor::buildstructor] impl Configuration { #[builder] + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder pub(crate) fn fake_new( supergraph: Option, health_check: Option, @@ -673,6 +675,7 @@ fn default_defer_support() -> bool { #[buildstructor::buildstructor] impl Supergraph { #[builder] + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder pub(crate) fn new( listen: Option, path: Option, @@ -701,6 +704,7 @@ impl Supergraph { #[buildstructor::buildstructor] impl Supergraph { #[builder] + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder pub(crate) fn fake_new( listen: Option, path: Option, diff --git a/apollo-router/src/configuration/subgraph.rs b/apollo-router/src/configuration/subgraph.rs index 1c3ea1d7db..cb00010d42 100644 --- a/apollo-router/src/configuration/subgraph.rs +++ b/apollo-router/src/configuration/subgraph.rs @@ -225,7 +225,7 @@ impl<'de> Deserialize<'de> for Field { struct FieldVisitor; -impl<'de> Visitor<'de> for FieldVisitor { +impl Visitor<'_> for FieldVisitor { type Value = Field; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { diff --git a/apollo-router/src/context/extensions/sync.rs b/apollo-router/src/context/extensions/sync.rs index 59c11c2b7a..9d36dfe5d3 100644 --- a/apollo-router/src/context/extensions/sync.rs +++ b/apollo-router/src/context/extensions/sync.rs @@ -58,7 +58,7 @@ impl<'a> ExtensionsGuard<'a> { } } -impl<'a> Deref for ExtensionsGuard<'a> { +impl Deref for ExtensionsGuard<'_> { type Target = super::Extensions; fn deref(&self) -> &super::Extensions { diff --git a/apollo-router/src/graphql/request.rs b/apollo-router/src/graphql/request.rs index fc572f70cb..fcf80f4615 100644 --- a/apollo-router/src/graphql/request.rs +++ b/apollo-router/src/graphql/request.rs @@ -307,7 +307,7 @@ fn get_from_urlencoded_value<'a, T: Deserialize<'a>>( struct RequestFromBytesSeed<'data>(&'data Bytes); -impl<'data, 'de> DeserializeSeed<'de> for RequestFromBytesSeed<'data> { +impl<'de> DeserializeSeed<'de> for RequestFromBytesSeed<'_> { type Value = Request; fn deserialize(self, deserializer: D) -> Result @@ -329,7 +329,7 @@ impl<'data, 'de> DeserializeSeed<'de> for RequestFromBytesSeed<'data> { struct RequestVisitor<'data>(&'data Bytes); - impl<'data, 'de> serde::de::Visitor<'de> for RequestVisitor<'data> { + impl<'de> serde::de::Visitor<'de> for RequestVisitor<'_> { type Value = Request; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { diff --git a/apollo-router/src/http_ext.rs b/apollo-router/src/http_ext.rs index b30982ec1c..700cf19177 100644 --- a/apollo-router/src/http_ext.rs +++ b/apollo-router/src/http_ext.rs @@ -194,9 +194,8 @@ impl PartialEq for TryIntoHeaderName { impl Hash for TryIntoHeaderName { fn hash(&self, state: &mut H) { - match &self.result { - Ok(value) => value.hash(state), - Err(_) => {} + if let Ok(value) = &self.result { + value.hash(state) } } } diff --git a/apollo-router/src/json_ext.rs b/apollo-router/src/json_ext.rs index 7fe1f5e3f5..ceddacf7f2 100644 --- a/apollo-router/src/json_ext.rs +++ b/apollo-router/src/json_ext.rs @@ -168,9 +168,6 @@ pub(crate) trait ValueExt { #[track_caller] fn is_object_of_type(&self, schema: &Schema, maybe_type: &str) -> bool; - /// value type - fn json_type_name(&self) -> &'static str; - fn as_i32(&self) -> Option; } @@ -544,17 +541,6 @@ impl ValueExt for Value { }) } - fn json_type_name(&self) -> &'static str { - match self { - Value::Array(_) => "array", - Value::Null => "null", - Value::Bool(_) => "boolean", - Value::Number(_) => "number", - Value::String(_) => "string", - Value::Object(_) => "object", - } - } - fn as_i32(&self) -> Option { self.as_i64()?.to_i32() } @@ -852,7 +838,7 @@ where struct FlattenVisitor; -impl<'de> serde::de::Visitor<'de> for FlattenVisitor { +impl serde::de::Visitor<'_> for FlattenVisitor { type Value = Option; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { @@ -903,7 +889,7 @@ where struct KeyVisitor; -impl<'de> serde::de::Visitor<'de> for KeyVisitor { +impl serde::de::Visitor<'_> for KeyVisitor { type Value = (String, Option); fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { @@ -947,7 +933,7 @@ where struct FragmentVisitor; -impl<'de> serde::de::Visitor<'de> for FragmentVisitor { +impl serde::de::Visitor<'_> for FragmentVisitor { type Value = String; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { diff --git a/apollo-router/src/lib.rs b/apollo-router/src/lib.rs index 2a62b7c25b..4d32f99259 100644 --- a/apollo-router/src/lib.rs +++ b/apollo-router/src/lib.rs @@ -19,8 +19,6 @@ #![cfg_attr(feature = "failfast", allow(unreachable_code))] #![warn(unreachable_pub)] #![warn(missing_docs)] -// TODO: silence false positives (apollo_compiler::Name) and investigate the rest -#![allow(clippy::mutable_key_type)] macro_rules! failfast_debug { ($($tokens:tt)+) => {{ diff --git a/apollo-router/src/metrics/layer.rs b/apollo-router/src/metrics/layer.rs index 8502e1da0d..2ecbeb5f6d 100644 --- a/apollo-router/src/metrics/layer.rs +++ b/apollo-router/src/metrics/layer.rs @@ -169,7 +169,7 @@ pub(crate) struct MetricVisitor<'a> { attributes_ignored: bool, } -impl<'a> MetricVisitor<'a> { +impl MetricVisitor<'_> { fn set_metric(&mut self, name: &'static str, instrument_type: InstrumentType) { self.metric = Some((name, instrument_type)); if self.attributes_ignored { @@ -181,7 +181,7 @@ impl<'a> MetricVisitor<'a> { } } -impl<'a> Visit for MetricVisitor<'a> { +impl Visit for MetricVisitor<'_> { fn record_f64(&mut self, field: &Field, value: f64) { if let Some(metric_name) = field.name().strip_prefix(METRIC_PREFIX_MONOTONIC_COUNTER) { self.set_metric(metric_name, InstrumentType::CounterF64(value)); @@ -416,7 +416,7 @@ impl<'a> Visit for MetricVisitor<'a> { } } -impl<'a> MetricVisitor<'a> { +impl MetricVisitor<'_> { fn finish(self) { if let Some((metric_name, instrument_type)) = self.metric { self.instruments.update_metric( diff --git a/apollo-router/src/metrics/mod.rs b/apollo-router/src/metrics/mod.rs index 1cb90bc2ae..3828f43d6f 100644 --- a/apollo-router/src/metrics/mod.rs +++ b/apollo-router/src/metrics/mod.rs @@ -617,7 +617,6 @@ macro_rules! f64_counter { /// * Imperfect mapping to metrics API that can only be checked at runtime. /// /// New metrics should be added using these macros. - #[allow(unused_macros)] macro_rules! i64_up_down_counter { ($($name:ident).+, $description:literal, $value: expr, $($attr_key:literal = $attr_value:expr),+) => { diff --git a/apollo-router/src/plugin/serde.rs b/apollo-router/src/plugin/serde.rs index 390b871e54..51f452d964 100644 --- a/apollo-router/src/plugin/serde.rs +++ b/apollo-router/src/plugin/serde.rs @@ -112,7 +112,7 @@ where #[derive(Default)] struct HeaderNameVisitor; -impl<'de> Visitor<'de> for HeaderNameVisitor { +impl Visitor<'_> for HeaderNameVisitor { type Value = HeaderName; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { @@ -137,7 +137,7 @@ where struct JSONQueryVisitor; -impl<'de> Visitor<'de> for JSONQueryVisitor { +impl Visitor<'_> for JSONQueryVisitor { type Value = JSONQuery; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { @@ -163,7 +163,7 @@ where struct HeaderValueVisitor; -impl<'de> Visitor<'de> for HeaderValueVisitor { +impl Visitor<'_> for HeaderValueVisitor { type Value = HeaderValue; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { @@ -193,7 +193,7 @@ where { struct RegexVisitor; - impl<'de> Visitor<'de> for RegexVisitor { + impl Visitor<'_> for RegexVisitor { type Value = Regex; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { @@ -221,7 +221,7 @@ where struct JSONPathVisitor; -impl<'de> serde::de::Visitor<'de> for JSONPathVisitor { +impl serde::de::Visitor<'_> for JSONPathVisitor { type Value = serde_json_bytes::path::JsonPathInst; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { diff --git a/apollo-router/src/plugins/authentication/jwks.rs b/apollo-router/src/plugins/authentication/jwks.rs index 9da1a2ac74..c366d73497 100644 --- a/apollo-router/src/plugins/authentication/jwks.rs +++ b/apollo-router/src/plugins/authentication/jwks.rs @@ -237,7 +237,7 @@ pub(super) struct Iter<'a> { list: Vec, } -impl<'a> Iterator for Iter<'a> { +impl Iterator for Iter<'_> { type Item = JwkSetInfo; fn next(&mut self) -> Option { diff --git a/apollo-router/src/plugins/authorization/authenticated.rs b/apollo-router/src/plugins/authorization/authenticated.rs index bfcb28c51a..f2876c6641 100644 --- a/apollo-router/src/plugins/authorization/authenticated.rs +++ b/apollo-router/src/plugins/authorization/authenticated.rs @@ -96,7 +96,7 @@ impl<'a> AuthenticatedCheckVisitor<'a> { } } -impl<'a> traverse::Visitor for AuthenticatedCheckVisitor<'a> { +impl traverse::Visitor for AuthenticatedCheckVisitor<'_> { fn operation(&mut self, root_type: &str, node: &executable::Operation) -> Result<(), BoxError> { if !self.entity_query { traverse::operation(self, root_type, node) @@ -319,7 +319,7 @@ impl<'a> AuthenticatedVisitor<'a> { } } -impl<'a> transform::Visitor for AuthenticatedVisitor<'a> { +impl transform::Visitor for AuthenticatedVisitor<'_> { fn operation( &mut self, root_type: &str, @@ -627,7 +627,7 @@ mod tests { paths: Vec, } - impl<'a> std::fmt::Display for TestResult<'a> { + impl std::fmt::Display for TestResult<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, diff --git a/apollo-router/src/plugins/authorization/policy.rs b/apollo-router/src/plugins/authorization/policy.rs index e317b7eb97..cbfd49e9ad 100644 --- a/apollo-router/src/plugins/authorization/policy.rs +++ b/apollo-router/src/plugins/authorization/policy.rs @@ -122,7 +122,7 @@ fn policy_argument( .filter_map(|v| v.as_str().map(str::to_owned)) } -impl<'a> traverse::Visitor for PolicyExtractionVisitor<'a> { +impl traverse::Visitor for PolicyExtractionVisitor<'_> { fn operation(&mut self, root_type: &str, node: &executable::Operation) -> Result<(), BoxError> { if let Some(ty) = self.schema.types.get(root_type) { self.extracted_policies.extend(policy_argument( @@ -417,7 +417,7 @@ impl<'a> PolicyFilteringVisitor<'a> { } } -impl<'a> transform::Visitor for PolicyFilteringVisitor<'a> { +impl transform::Visitor for PolicyFilteringVisitor<'_> { fn operation( &mut self, root_type: &str, @@ -763,7 +763,7 @@ mod tests { paths: Vec, } - impl<'a> std::fmt::Display for TestResult<'a> { + impl std::fmt::Display for TestResult<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index 6dcccfc0a0..0552cee25e 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -122,7 +122,7 @@ fn scopes_argument( .filter_map(|value| value.as_str().map(str::to_owned)) } -impl<'a> traverse::Visitor for ScopeExtractionVisitor<'a> { +impl traverse::Visitor for ScopeExtractionVisitor<'_> { fn operation(&mut self, root_type: &str, node: &executable::Operation) -> Result<(), BoxError> { if let Some(ty) = self.schema.types.get(root_type) { self.extracted_scopes.extend(scopes_argument( @@ -418,7 +418,7 @@ impl<'a> ScopeFilteringVisitor<'a> { } } -impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { +impl transform::Visitor for ScopeFilteringVisitor<'_> { fn operation( &mut self, root_type: &str, @@ -767,7 +767,7 @@ mod tests { paths: Vec, } - impl<'a> std::fmt::Display for TestResult<'a> { + impl std::fmt::Display for TestResult<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, diff --git a/apollo-router/src/plugins/cache/cache_control.rs b/apollo-router/src/plugins/cache/cache_control.rs index 4aa300e077..6cb812e17d 100644 --- a/apollo-router/src/plugins/cache/cache_control.rs +++ b/apollo-router/src/plugins/cache/cache_control.rs @@ -258,11 +258,7 @@ impl CacheControl { fn update_ttl(&self, ttl: u32, now: u64) -> u32 { let elapsed = self.elapsed_inner(now); - if elapsed >= ttl { - 0 - } else { - ttl - elapsed - } + ttl.saturating_sub(elapsed) } pub(crate) fn merge(&self, other: &CacheControl) -> CacheControl { @@ -376,11 +372,7 @@ impl CacheControl { pub(crate) fn remaining_time(&self, now: u64) -> Option { self.ttl().map(|ttl| { let elapsed = self.elapsed_inner(now); - if ttl > elapsed { - ttl - elapsed - } else { - 0 - } + ttl.saturating_sub(elapsed) }) } } diff --git a/apollo-router/src/plugins/cache/invalidation.rs b/apollo-router/src/plugins/cache/invalidation.rs index 8e30a8a830..387f22fe14 100644 --- a/apollo-router/src/plugins/cache/invalidation.rs +++ b/apollo-router/src/plugins/cache/invalidation.rs @@ -50,9 +50,6 @@ impl std::fmt::Display for InvalidationErrors { impl std::error::Error for InvalidationErrors {} -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub(crate) struct InvalidationTopic; - #[derive(Clone, Debug, PartialEq)] pub(crate) enum InvalidationOrigin { Endpoint, 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 d212501660..90b1ee95e4 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 @@ -561,7 +561,7 @@ impl<'schema> ResponseCostCalculator<'schema> { } } -impl<'schema> ResponseVisitor for ResponseCostCalculator<'schema> { +impl ResponseVisitor for ResponseCostCalculator<'_> { fn visit_field( &mut self, request: &ExecutableDocument, diff --git a/apollo-router/src/plugins/demand_control/mod.rs b/apollo-router/src/plugins/demand_control/mod.rs index 8e8d419ab8..f2348f31a9 100644 --- a/apollo-router/src/plugins/demand_control/mod.rs +++ b/apollo-router/src/plugins/demand_control/mod.rs @@ -193,7 +193,7 @@ impl From> for DemandControlError { } } -impl<'a> From> for DemandControlError { +impl From> for DemandControlError { fn from(value: FieldLookupError) -> Self { match value { FieldLookupError::NoSuchType => DemandControlError::QueryParseFailure( diff --git a/apollo-router/src/plugins/fleet_detector.rs b/apollo-router/src/plugins/fleet_detector.rs index 46fadedd55..27b01e1527 100644 --- a/apollo-router/src/plugins/fleet_detector.rs +++ b/apollo-router/src/plugins/fleet_detector.rs @@ -66,7 +66,8 @@ enum GaugeStore { #[default] Disabled, Pending, - Active(Vec>), + // This `Vec` is not used explicitly but is to be kept alive until the enum is dropped + Active(#[allow(unused)] Vec>), } impl GaugeStore { diff --git a/apollo-router/src/plugins/headers.rs b/apollo-router/src/plugins/headers.rs index b717eef838..5be0c352d4 100644 --- a/apollo-router/src/plugins/headers.rs +++ b/apollo-router/src/plugins/headers.rs @@ -1051,7 +1051,7 @@ mod test { } impl SubgraphRequest { - pub fn assert_headers(&self, headers: Vec<(&'static str, &'static str)>) -> bool { + fn assert_headers(&self, headers: Vec<(&'static str, &'static str)>) -> bool { let mut headers = headers.clone(); headers.push((HOST.as_str(), "rhost")); headers.push((CONTENT_LENGTH.as_str(), "22")); diff --git a/apollo-router/src/plugins/include_subgraph_errors.rs b/apollo-router/src/plugins/include_subgraph_errors.rs index 5bc29a3237..34e4196bfe 100644 --- a/apollo-router/src/plugins/include_subgraph_errors.rs +++ b/apollo-router/src/plugins/include_subgraph_errors.rs @@ -47,7 +47,7 @@ impl Plugin for IncludeSubgraphErrors { let sub_name_response = name.to_string(); let sub_name_error = name.to_string(); - return service + service .map_response(move |mut response: SubgraphResponse| { let errors = &mut response.response.body_mut().errors; if !errors.is_empty() { @@ -82,7 +82,7 @@ impl Plugin for IncludeSubgraphErrors { }) } }) - .boxed(); + .boxed() } } diff --git a/apollo-router/src/plugins/progressive_override/visitor.rs b/apollo-router/src/plugins/progressive_override/visitor.rs index d17cd07aec..697a9dbbe0 100644 --- a/apollo-router/src/plugins/progressive_override/visitor.rs +++ b/apollo-router/src/plugins/progressive_override/visitor.rs @@ -29,7 +29,7 @@ impl<'a> OverrideLabelVisitor<'a> { } } -impl<'a> traverse::Visitor for OverrideLabelVisitor<'a> { +impl traverse::Visitor for OverrideLabelVisitor<'_> { fn schema(&self) -> &apollo_compiler::Schema { self.schema } diff --git a/apollo-router/src/plugins/telemetry/config_new/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/attributes.rs index e21626200f..60179c5c20 100644 --- a/apollo-router/src/plugins/telemetry/config_new/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/attributes.rs @@ -1149,7 +1149,7 @@ impl<'a> SubgraphRequestResendCountKey<'a> { } } -impl<'a> From> for String { +impl From> for String { fn from(value: SubgraphRequestResendCountKey) -> Self { format!( "apollo::telemetry::http_request_resend_count_{}", diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs b/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs index c73d9ec099..a6d8621cb2 100644 --- a/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs @@ -167,7 +167,7 @@ struct GraphQLInstrumentsVisitor<'a> { instruments: &'a GraphQLInstruments, } -impl<'a> ResponseVisitor for GraphQLInstrumentsVisitor<'a> { +impl ResponseVisitor for GraphQLInstrumentsVisitor<'_> { fn visit_field( &mut self, request: &ExecutableDocument, diff --git a/apollo-router/src/plugins/telemetry/endpoint.rs b/apollo-router/src/plugins/telemetry/endpoint.rs index b5af0ede1e..0314aab904 100644 --- a/apollo-router/src/plugins/telemetry/endpoint.rs +++ b/apollo-router/src/plugins/telemetry/endpoint.rs @@ -72,7 +72,7 @@ impl<'de> Deserialize<'de> for UriEndpoint { fn deserialize>(deserializer: D) -> Result { struct EndpointVisitor; - impl<'de> Visitor<'de> for EndpointVisitor { + impl Visitor<'_> for EndpointVisitor { type Value = UriEndpoint; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { @@ -134,7 +134,7 @@ impl<'de> Deserialize<'de> for SocketEndpoint { fn deserialize>(deserializer: D) -> Result { struct EndpointVisitor; - impl<'de> Visitor<'de> for EndpointVisitor { + impl Visitor<'_> for EndpointVisitor { type Value = SocketEndpoint; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { diff --git a/apollo-router/src/plugins/telemetry/fmt_layer.rs b/apollo-router/src/plugins/telemetry/fmt_layer.rs index cf5eb49c8f..c1909bb0ce 100644 --- a/apollo-router/src/plugins/telemetry/fmt_layer.rs +++ b/apollo-router/src/plugins/telemetry/fmt_layer.rs @@ -196,7 +196,7 @@ pub(crate) struct FieldsVisitor<'a, 'b> { excluded_attributes: &'b HashSet<&'static str>, } -impl<'a, 'b> FieldsVisitor<'a, 'b> { +impl<'b> FieldsVisitor<'_, 'b> { fn new(excluded_attributes: &'b HashSet<&'static str>) -> Self { Self { values: HashMap::with_capacity(0), @@ -205,7 +205,7 @@ impl<'a, 'b> FieldsVisitor<'a, 'b> { } } -impl<'a, 'b> field::Visit for FieldsVisitor<'a, 'b> { +impl field::Visit for FieldsVisitor<'_, '_> { /// Visit a double precision floating point value. fn record_f64(&mut self, field: &Field, value: f64) { self.values @@ -375,7 +375,7 @@ subgraph: } struct Guard<'a>(MutexGuard<'a, Vec>); - impl<'a> std::io::Write for Guard<'a> { + impl std::io::Write for Guard<'_> { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.0.write(buf) } diff --git a/apollo-router/src/plugins/telemetry/formatters/json.rs b/apollo-router/src/plugins/telemetry/formatters/json.rs index 7bb94bfebd..7062cf6254 100644 --- a/apollo-router/src/plugins/telemetry/formatters/json.rs +++ b/apollo-router/src/plugins/telemetry/formatters/json.rs @@ -60,7 +60,7 @@ impl Default for Json { struct SerializableResources<'a>(&'a Vec<(String, serde_json::Value)>); -impl<'a> serde::ser::Serialize for SerializableResources<'a> { +impl serde::ser::Serialize for SerializableResources<'_> { fn serialize(&self, serializer_o: Ser) -> Result where Ser: serde::ser::Serializer, @@ -79,7 +79,7 @@ struct SerializableContext<'a, 'b, Span>(Option>, &'b HashSet< where Span: Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>; -impl<'a, 'b, Span> serde::ser::Serialize for SerializableContext<'a, 'b, Span> +impl serde::ser::Serialize for SerializableContext<'_, '_, Span> where Span: Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>, { @@ -108,7 +108,7 @@ struct SerializableSpan<'a, 'b, Span>( where Span: for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>; -impl<'a, 'b, Span> serde::ser::Serialize for SerializableSpan<'a, 'b, Span> +impl serde::ser::Serialize for SerializableSpan<'_, '_, Span> where Span: for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>, { @@ -449,7 +449,7 @@ impl<'a> WriteAdaptor<'a> { } } -impl<'a> io::Write for WriteAdaptor<'a> { +impl io::Write for WriteAdaptor<'_> { fn write(&mut self, buf: &[u8]) -> io::Result { let s = std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; @@ -466,7 +466,7 @@ impl<'a> io::Write for WriteAdaptor<'a> { } } -impl<'a> fmt::Debug for WriteAdaptor<'a> { +impl fmt::Debug for WriteAdaptor<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.pad("WriteAdaptor { .. }") } diff --git a/apollo-router/src/plugins/telemetry/formatters/text.rs b/apollo-router/src/plugins/telemetry/formatters/text.rs index d809496964..e26768146c 100644 --- a/apollo-router/src/plugins/telemetry/formatters/text.rs +++ b/apollo-router/src/plugins/telemetry/formatters/text.rs @@ -422,7 +422,7 @@ impl<'a> FmtThreadName<'a> { } } -impl<'a> fmt::Display for FmtThreadName<'a> { +impl fmt::Display for FmtThreadName<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering::AcqRel; @@ -572,7 +572,7 @@ impl<'a> DefaultVisitor<'a> { } } -impl<'a> field::Visit for DefaultVisitor<'a> { +impl field::Visit for DefaultVisitor<'_> { fn record_str(&mut self, field: &Field, value: &str) { if self.result.is_err() { return; @@ -609,13 +609,13 @@ impl<'a> field::Visit for DefaultVisitor<'a> { } } -impl<'a> VisitOutput for DefaultVisitor<'a> { +impl VisitOutput for DefaultVisitor<'_> { fn finish(self) -> fmt::Result { self.result } } -impl<'a> VisitFmt for DefaultVisitor<'a> { +impl VisitFmt for DefaultVisitor<'_> { fn writer(&mut self) -> &mut dyn fmt::Write { &mut self.writer } @@ -624,7 +624,7 @@ impl<'a> VisitFmt for DefaultVisitor<'a> { /// Renders an error into a list of sources, *including* the error struct ErrorSourceList<'a>(&'a (dyn std::error::Error + 'static)); -impl<'a> std::fmt::Display for ErrorSourceList<'a> { +impl std::fmt::Display for ErrorSourceList<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut list = f.debug_list(); let mut curr = Some(self.0); diff --git a/apollo-router/src/plugins/telemetry/metrics/span_metrics_exporter.rs b/apollo-router/src/plugins/telemetry/metrics/span_metrics_exporter.rs index 6129b6b9e4..b142a90736 100644 --- a/apollo-router/src/plugins/telemetry/metrics/span_metrics_exporter.rs +++ b/apollo-router/src/plugins/telemetry/metrics/span_metrics_exporter.rs @@ -155,7 +155,7 @@ struct ValueVisitor<'a> { timings: &'a mut Timings, } -impl<'a> Visit for ValueVisitor<'a> { +impl Visit for ValueVisitor<'_> { fn record_debug(&mut self, _field: &Field, _value: &dyn std::fmt::Debug) {} fn record_str(&mut self, field: &Field, value: &str) { diff --git a/apollo-router/src/plugins/telemetry/otel/layer.rs b/apollo-router/src/plugins/telemetry/otel/layer.rs index 309114b20f..6beb2cc59c 100644 --- a/apollo-router/src/plugins/telemetry/otel/layer.rs +++ b/apollo-router/src/plugins/telemetry/otel/layer.rs @@ -157,7 +157,7 @@ struct SpanEventVisitor<'a, 'b> { custom_event: bool, } -impl<'a, 'b> field::Visit for SpanEventVisitor<'a, 'b> { +impl field::Visit for SpanEventVisitor<'_, '_> { /// Record events on the underlying OpenTelemetry [`Span`] from `bool` values. /// /// [`Span`]: opentelemetry::trace::Span @@ -318,7 +318,7 @@ struct SpanAttributeVisitor<'a> { exception_config: ExceptionFieldConfig, } -impl<'a> SpanAttributeVisitor<'a> { +impl SpanAttributeVisitor<'_> { fn record(&mut self, attribute: KeyValue) { debug_assert!(self.span_builder.attributes.is_some()); if let Some(v) = self.span_builder.attributes.as_mut() { @@ -327,7 +327,7 @@ impl<'a> SpanAttributeVisitor<'a> { } } -impl<'a> field::Visit for SpanAttributeVisitor<'a> { +impl field::Visit for SpanAttributeVisitor<'_> { /// Set attributes on the underlying OpenTelemetry [`Span`] from `bool` values. /// /// [`Span`]: opentelemetry::trace::Span diff --git a/apollo-router/src/plugins/telemetry/tracing/apollo_telemetry.rs b/apollo-router/src/plugins/telemetry/tracing/apollo_telemetry.rs index fc343c6b4d..123cfbc8a2 100644 --- a/apollo-router/src/plugins/telemetry/tracing/apollo_telemetry.rs +++ b/apollo-router/src/plugins/telemetry/tracing/apollo_telemetry.rs @@ -293,6 +293,7 @@ enum TreeData { #[buildstructor::buildstructor] impl Exporter { #[builder] + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder pub(crate) fn new<'a>( endpoint: &'a Url, otlp_endpoint: &'a Url, diff --git a/apollo-router/src/plugins/telemetry/tracing/datadog.rs b/apollo-router/src/plugins/telemetry/tracing/datadog.rs index 4574b529ff..d0994fbf13 100644 --- a/apollo-router/src/plugins/telemetry/tracing/datadog.rs +++ b/apollo-router/src/plugins/telemetry/tracing/datadog.rs @@ -158,7 +158,7 @@ impl TracingConfigurator for Config { return value.as_str(); } } - return span.name.as_ref(); + span.name.as_ref() }) }) .with_name_mapping(move |span, _model_config| { diff --git a/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/intern.rs b/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/intern.rs index fd1f69375f..5db2873eae 100644 --- a/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/intern.rs +++ b/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/intern.rs @@ -16,7 +16,7 @@ pub(crate) enum InternValue<'a> { OpenTelemetryValue(&'a Value), } -impl<'a> Hash for InternValue<'a> { +impl Hash for InternValue<'_> { fn hash(&self, state: &mut H) { match &self { InternValue::RegularString(s) => s.hash(state), @@ -40,7 +40,7 @@ impl<'a> Hash for InternValue<'a> { } } -impl<'a> Eq for InternValue<'a> {} +impl Eq for InternValue<'_> {} const BOOLEAN_TRUE: &str = "true"; const BOOLEAN_FALSE: &str = "false"; @@ -80,7 +80,7 @@ impl WriteAsLiteral for StringValue { } } -impl<'a> InternValue<'a> { +impl InternValue<'_> { pub(crate) fn write_as_str( &self, payload: &mut W, diff --git a/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/mod.rs b/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/mod.rs index ae4a37ba07..43b5a5d75d 100644 --- a/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/mod.rs +++ b/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/mod.rs @@ -169,26 +169,7 @@ impl Default for DatadogPipelineBuilder { mapping: Mapping::empty(), api_version: ApiVersion::Version05, unified_tags: UnifiedTags::new(), - #[cfg(all( - not(feature = "reqwest-client"), - not(feature = "reqwest-blocking-client"), - not(feature = "surf-client"), - ))] client: None, - #[cfg(all( - not(feature = "reqwest-client"), - not(feature = "reqwest-blocking-client"), - feature = "surf-client" - ))] - client: Some(Arc::new(surf::Client::new())), - #[cfg(all( - not(feature = "surf-client"), - not(feature = "reqwest-blocking-client"), - feature = "reqwest-client" - ))] - client: Some(Arc::new(reqwest::Client::new())), - #[cfg(feature = "reqwest-blocking-client")] - client: Some(Arc::new(reqwest::blocking::Client::new())), } } } diff --git a/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/model/unified_tags.rs b/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/model/unified_tags.rs index e4e835c550..d047b95e23 100644 --- a/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/model/unified_tags.rs +++ b/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/exporter/model/unified_tags.rs @@ -1,4 +1,4 @@ -/// Unified tags - See: https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging +//! Unified tags - See: pub struct UnifiedTags { pub service: UnifiedTagField, diff --git a/apollo-router/src/plugins/telemetry/utils.rs b/apollo-router/src/plugins/telemetry/utils.rs index 42b57f122c..2081748dcb 100644 --- a/apollo-router/src/plugins/telemetry/utils.rs +++ b/apollo-router/src/plugins/telemetry/utils.rs @@ -1,22 +1,6 @@ use std::time::Duration; use std::time::Instant; -use tracing_core::field::Value; - -pub(crate) trait TracingUtils { - fn or_empty(&self) -> &dyn Value; -} - -impl TracingUtils for bool { - fn or_empty(&self) -> &dyn Value { - if *self { - self as &dyn Value - } else { - &::tracing::field::Empty - } - } -} - /// Timer implementing Drop to automatically compute the duration between the moment it has been created until it's dropped ///```ignore /// Timer::new(|duration| { diff --git a/apollo-router/src/query_planner/execution.rs b/apollo-router/src/query_planner/execution.rs index 9a5b647350..12acef865b 100644 --- a/apollo-router/src/query_planner/execution.rs +++ b/apollo-router/src/query_planner/execution.rs @@ -122,7 +122,7 @@ impl PlanNode { current_dir: &'a Path, parent_value: &'a Value, sender: mpsc::Sender, - ) -> future::BoxFuture<(Value, Vec)> { + ) -> future::BoxFuture<'a, (Value, Vec)> { Box::pin(async move { tracing::trace!("executing plan:\n{:#?}", self); let mut value; diff --git a/apollo-router/src/query_planner/subgraph_context.rs b/apollo-router/src/query_planner/subgraph_context.rs index e841cf27c3..6b3059f272 100644 --- a/apollo-router/src/query_planner/subgraph_context.rs +++ b/apollo-router/src/query_planner/subgraph_context.rs @@ -229,7 +229,7 @@ pub(crate) fn build_operation_with_aliasing( return ed .validate(subgraph_schema) - .map_err(ContextBatchingError::InvalidDocumentGenerated); + .map_err(|e| ContextBatchingError::InvalidDocumentGenerated(Box::new(e))); } Err(ContextBatchingError::NoSelectionSet) } @@ -348,7 +348,7 @@ fn transform_field_arguments( pub(crate) enum ContextBatchingError { NoSelectionSet, // The only use of the field is in `Debug`, on purpose. - InvalidDocumentGenerated(#[allow(unused)] WithErrors), + InvalidDocumentGenerated(#[allow(unused)] Box>), InvalidRelativePath, UnexpectedSelection, } diff --git a/apollo-router/src/query_planner/subscription.rs b/apollo-router/src/query_planner/subscription.rs index 31f4a37319..9bb515b958 100644 --- a/apollo-router/src/query_planner/subscription.rs +++ b/apollo-router/src/query_planner/subscription.rs @@ -85,7 +85,7 @@ impl SubscriptionNode { current_dir: &'a Path, parent_value: &'a Value, sender: tokio::sync::mpsc::Sender, - ) -> future::BoxFuture> { + ) -> future::BoxFuture<'a, Vec> { if parameters.subscription_handle.is_none() { tracing::error!("No subscription handle provided for a subscription"); return Box::pin(async { diff --git a/apollo-router/src/services/external.rs b/apollo-router/src/services/external.rs index 58bc24832d..cec76f3413 100644 --- a/apollo-router/src/services/external.rs +++ b/apollo-router/src/services/external.rs @@ -117,6 +117,7 @@ where /// This is the constructor (or builder) to use when constructing a Router /// `Externalizable`. /// + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder fn router_new( stage: PipelineStep, control: Option, @@ -157,6 +158,7 @@ where /// This is the constructor (or builder) to use when constructing a Supergraph /// `Externalizable`. /// + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder fn supergraph_new( stage: PipelineStep, control: Option, @@ -197,6 +199,7 @@ where /// This is the constructor (or builder) to use when constructing an Execution /// `Externalizable`. /// + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder fn execution_new( stage: PipelineStep, control: Option, @@ -238,6 +241,7 @@ where /// This is the constructor (or builder) to use when constructing a Subgraph /// `Externalizable`. /// + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder fn subgraph_new( stage: PipelineStep, control: Option, diff --git a/apollo-router/src/services/layers/query_analysis.rs b/apollo-router/src/services/layers/query_analysis.rs index fbafb49a51..7af0a11ec0 100644 --- a/apollo-router/src/services/layers/query_analysis.rs +++ b/apollo-router/src/services/layers/query_analysis.rs @@ -266,9 +266,6 @@ pub(crate) struct ParsedDocumentInner { pub(crate) has_explicit_root_fields: bool, } -#[derive(Debug)] -pub(crate) struct RootFieldKinds {} - impl ParsedDocumentInner { pub(crate) fn new( ast: ast::Document, diff --git a/apollo-router/src/services/subgraph.rs b/apollo-router/src/services/subgraph.rs index 987eef24a0..ad3e431c9e 100644 --- a/apollo-router/src/services/subgraph.rs +++ b/apollo-router/src/services/subgraph.rs @@ -238,6 +238,7 @@ impl Response { /// The parameters are not optional, because in a live situation all of these properties must be /// set and be correct to create a Response. #[builder(visibility = "pub")] + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder fn new( label: Option, data: Option, @@ -286,6 +287,7 @@ impl Response { /// Response. It's usually enough for testing, when a fully constructed Response is /// difficult to construct and not required for the purposes of the test. #[builder(visibility = "pub")] + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder fn fake_new( label: Option, data: Option, @@ -320,6 +322,7 @@ impl Response { /// Response. It's usually enough for testing, when a fully constructed Response is /// difficult to construct and not required for the purposes of the test. #[builder(visibility = "pub")] + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder fn fake2_new( label: Option, data: Option, diff --git a/apollo-router/src/services/subgraph_service.rs b/apollo-router/src/services/subgraph_service.rs index dc93499143..8b6954b5ce 100644 --- a/apollo-router/src/services/subgraph_service.rs +++ b/apollo-router/src/services/subgraph_service.rs @@ -1939,7 +1939,7 @@ mod tests { .unwrap()); } - return Ok(http::Response::builder() + Ok(http::Response::builder() .header(CONTENT_TYPE, APPLICATION_JSON.essence_str()) .status(StatusCode::OK) .body( @@ -1950,7 +1950,7 @@ mod tests { .expect("always valid") .into(), ) - .unwrap()); + .unwrap()) } Err(_) => { panic!("invalid graphql request recieved") @@ -1996,7 +1996,7 @@ mod tests { .unwrap()); } - return Ok(http::Response::builder() + Ok(http::Response::builder() .header(CONTENT_TYPE, APPLICATION_JSON.essence_str()) .status(StatusCode::OK) .body( @@ -2007,7 +2007,7 @@ mod tests { .expect("always valid") .into(), ) - .unwrap()); + .unwrap()) } Err(_) => { panic!("invalid graphql request recieved") @@ -2038,7 +2038,7 @@ mod tests { } if request.query.is_none() { - return Ok(http::Response::builder() + Ok(http::Response::builder() .header(CONTENT_TYPE, APPLICATION_JSON.essence_str()) .status(StatusCode::OK) .body( @@ -2053,9 +2053,9 @@ mod tests { .expect("always valid") .into(), ) - .unwrap()); + .unwrap()) } else { - return Ok(http::Response::builder() + Ok(http::Response::builder() .header(CONTENT_TYPE, APPLICATION_JSON.essence_str()) .status(StatusCode::OK) .body( @@ -2066,7 +2066,7 @@ mod tests { .expect("always valid") .into(), ) - .unwrap()); + .unwrap()) } } Err(_) => { @@ -2098,7 +2098,7 @@ mod tests { } if request.query.is_none() { - return Ok(http::Response::builder() + Ok(http::Response::builder() .header(CONTENT_TYPE, APPLICATION_JSON.essence_str()) .status(StatusCode::OK) .body( @@ -2113,9 +2113,9 @@ mod tests { .expect("always valid") .into(), ) - .unwrap()); + .unwrap()) } else { - return Ok(http::Response::builder() + Ok(http::Response::builder() .header(CONTENT_TYPE, APPLICATION_JSON.essence_str()) .status(StatusCode::OK) .body( @@ -2126,7 +2126,7 @@ mod tests { .expect("always valid") .into(), ) - .unwrap()); + .unwrap()) } } Err(_) => { @@ -2157,7 +2157,7 @@ mod tests { panic!("persistedQuery expected when configuration has apq_enabled=true") } - return Ok(http::Response::builder() + Ok(http::Response::builder() .header(CONTENT_TYPE, APPLICATION_JSON.essence_str()) .status(StatusCode::OK) .body( @@ -2168,7 +2168,7 @@ mod tests { .expect("always valid") .into(), ) - .unwrap()); + .unwrap()) } Err(_) => { panic!("invalid graphql request recieved") @@ -2200,7 +2200,7 @@ mod tests { ) } - return Ok(http::Response::builder() + Ok(http::Response::builder() .header(CONTENT_TYPE, APPLICATION_JSON.essence_str()) .status(StatusCode::OK) .body( @@ -2211,7 +2211,7 @@ mod tests { .expect("always valid") .into(), ) - .unwrap()); + .unwrap()) } Err(_) => { panic!("invalid graphql request recieved") diff --git a/apollo-router/src/spec/field_type.rs b/apollo-router/src/spec/field_type.rs index b160aff214..54f7fafd3b 100644 --- a/apollo-router/src/spec/field_type.rs +++ b/apollo-router/src/spec/field_type.rs @@ -26,7 +26,7 @@ impl Serialize for FieldType { { struct BorrowedFieldType<'a>(&'a schema::Type); - impl<'a> Serialize for BorrowedFieldType<'a> { + impl Serialize for BorrowedFieldType<'_> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, diff --git a/apollo-router/src/spec/query/change.rs b/apollo-router/src/spec/query/change.rs index 73bda2881f..79e6369499 100644 --- a/apollo-router/src/spec/query/change.rs +++ b/apollo-router/src/spec/query/change.rs @@ -8,18 +8,18 @@ //! be using it. //! This algorithm is used in 2 places: //! * in the query planner cache: generating query plans can be expensive, so the -//! router has a warm up feature, where upon receving a new schema, it will take -//! the most used queries and plan them, before switching traffic to the new -//! schema. Generating all of those plans takes a lot of time. By using this -//! hashing algorithm, we can detect that the schema change does not affect the -//! query, which means that we can reuse the old query plan directly and avoid -//! the expensive planning task +//! router has a warm up feature, where upon receving a new schema, it will take +//! the most used queries and plan them, before switching traffic to the new +//! schema. Generating all of those plans takes a lot of time. By using this +//! hashing algorithm, we can detect that the schema change does not affect the +//! query, which means that we can reuse the old query plan directly and avoid +//! the expensive planning task //! * in entity caching: the responses returned by subgraphs can change depending -//! on the schema (example: a field moving from String to Int), so we need to -//! detect that. One way to do it was to add the schema hash to the cache key, but -//! as a result it wipes the cache on every schema update, which will cause -//! performance and reliability issues. With this hashing algorithm, cached entries -//! can be kept across schema updates +//! on the schema (example: a field moving from String to Int), so we need to +//! detect that. One way to do it was to add the schema hash to the cache key, but +//! as a result it wipes the cache on every schema update, which will cause +//! performance and reliability issues. With this hashing algorithm, cached entries +//! can be kept across schema updates //! //! ## Technical details //! @@ -665,7 +665,7 @@ impl<'a> QueryHashVisitor<'a> { } } -impl<'a> Hasher for QueryHashVisitor<'a> { +impl Hasher for QueryHashVisitor<'_> { fn finish(&self) -> u64 { unreachable!() } @@ -677,7 +677,7 @@ impl<'a> Hasher for QueryHashVisitor<'a> { } } -impl<'a> Visitor for QueryHashVisitor<'a> { +impl Visitor for QueryHashVisitor<'_> { fn operation(&mut self, root_type: &str, node: &executable::Operation) -> Result<(), BoxError> { "^VISIT_OPERATION".hash(self); diff --git a/apollo-router/src/spec/query/subselections.rs b/apollo-router/src/spec/query/subselections.rs index 2ec203144e..37339c995c 100644 --- a/apollo-router/src/spec/query/subselections.rs +++ b/apollo-router/src/spec/query/subselections.rs @@ -47,7 +47,7 @@ impl<'de> Deserialize<'de> for SubSelectionKey { } struct SubSelectionKeyVisitor; -impl<'de> Visitor<'de> for SubSelectionKeyVisitor { +impl Visitor<'_> for SubSelectionKeyVisitor { type Value = SubSelectionKey; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { diff --git a/apollo-router/src/spec/query/transform.rs b/apollo-router/src/spec/query/transform.rs index 8b31d6054b..47b93c75e3 100644 --- a/apollo-router/src/spec/query/transform.rs +++ b/apollo-router/src/spec/query/transform.rs @@ -707,7 +707,7 @@ fragment F on Query { result: ast::Document, } - impl<'a> std::fmt::Display for TestResult<'a> { + impl std::fmt::Display for TestResult<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "query:\n{}\nfiltered:\n{}", self.query, self.result,) } diff --git a/apollo-router/src/uplink/persisted_queries_manifest_stream.rs b/apollo-router/src/uplink/persisted_queries_manifest_stream.rs index 109aad3fc5..32d35eaa4d 100644 --- a/apollo-router/src/uplink/persisted_queries_manifest_stream.rs +++ b/apollo-router/src/uplink/persisted_queries_manifest_stream.rs @@ -19,7 +19,6 @@ use crate::uplink::UplinkResponse; response_derives = "PartialEq, Debug, Deserialize", deprecated = "warn" )] - pub(crate) struct PersistedQueriesManifestQuery; impl From for persisted_queries_manifest_query::Variables { diff --git a/apollo-router/src/uplink/schema_stream.rs b/apollo-router/src/uplink/schema_stream.rs index 376f600e74..d42d0b303d 100644 --- a/apollo-router/src/uplink/schema_stream.rs +++ b/apollo-router/src/uplink/schema_stream.rs @@ -19,7 +19,6 @@ use crate::uplink::UplinkResponse; response_derives = "PartialEq, Debug, Deserialize", deprecated = "warn" )] - pub(crate) struct SupergraphSdlQuery; impl From for supergraph_sdl_query::Variables { diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index 671c462519..7208a85c10 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -276,6 +276,7 @@ impl Telemetry { #[buildstructor] impl IntegrationTest { #[builder] + #[allow(clippy::too_many_arguments)] // not typically used directly, only defines the builder pub async fn new( config: String, telemetry: Option, diff --git a/apollo-router/tests/integration/telemetry/jaeger.rs b/apollo-router/tests/integration/telemetry/jaeger.rs index fcf59e4ef5..7d9dc1bf46 100644 --- a/apollo-router/tests/integration/telemetry/jaeger.rs +++ b/apollo-router/tests/integration/telemetry/jaeger.rs @@ -486,8 +486,7 @@ fn verify_root_span_fields(trace: &Value, operation_name: Option<&str>) -> Resul } else { assert!(request_span .select_path("$.tags[?(@.key == 'graphql.operation.name')].value")? - .first() - .is_none(),); + .is_empty(),); } assert_eq!( @@ -519,8 +518,7 @@ fn verify_supergraph_span_fields( } else { assert!(supergraph_span .select_path("$.tags[?(@.key == 'graphql.operation.name')].value")? - .first() - .is_none(),); + .is_empty(),); } if custom_span_instrumentation { assert_eq!( diff --git a/dockerfiles/diy/dockerfiles/Dockerfile.repo b/dockerfiles/diy/dockerfiles/Dockerfile.repo index 210d165e07..cabfed825c 100644 --- a/dockerfiles/diy/dockerfiles/Dockerfile.repo +++ b/dockerfiles/diy/dockerfiles/Dockerfile.repo @@ -1,6 +1,6 @@ # Use the rust build image from docker as our base # renovate-automation: rustc version -FROM rust:1.76.0 as build +FROM rust:1.83.0 as build # Set our working directory for the build WORKDIR /usr/src/router diff --git a/docs/source/routing/customization/custom-binary.mdx b/docs/source/routing/customization/custom-binary.mdx index 87b87c2fae..8f2dbf1bbe 100644 --- a/docs/source/routing/customization/custom-binary.mdx +++ b/docs/source/routing/customization/custom-binary.mdx @@ -18,7 +18,7 @@ Learn how to compile a custom binary from Apollo Router Core source, which is re To compile the router, you need to have the following installed: -* [Rust 1.76.0 or later](https://www.rust-lang.org/tools/install) +* [Rust 1.83.0 or later](https://www.rust-lang.org/tools/install) * [Node.js 16.9.1 or later](https://nodejs.org/en/download/) * [CMake 3.5.1 or later](https://cmake.org/download/) diff --git a/examples/async-auth/rust/src/main.rs b/examples/async-auth/rust/src/main.rs index 98681f4b1b..61079910cc 100644 --- a/examples/async-auth/rust/src/main.rs +++ b/examples/async-auth/rust/src/main.rs @@ -1,3 +1,4 @@ +//! ```text //! curl -v \ //! --header 'content-type: application/json' \ //! --header 'x-client-id: unknown' \ @@ -22,6 +23,7 @@ //! < //! * Connection #0 to host 127.0.0.1 left intact //! {"data":{"me":{"name":"Ada Lovelace"}}} +//! ``` //! The only thing you need to add to your main.rs file is // adding the module to your main.rs file diff --git a/examples/forbid-anonymous-operations/rust/src/main.rs b/examples/forbid-anonymous-operations/rust/src/main.rs index 3f3fb95eb2..c3e20dfdd4 100644 --- a/examples/forbid-anonymous-operations/rust/src/main.rs +++ b/examples/forbid-anonymous-operations/rust/src/main.rs @@ -1,3 +1,4 @@ +//! ```text //! curl -v \ //! --header 'content-type: application/json' \ //! --url 'http://127.0.0.1:4000' \ @@ -9,6 +10,7 @@ //! < //! * Connection #0 to host 127.0.0.1 left intact //! {"errors":[{"message":"Anonymous operations are not allowed","locations":[],"path":null}]} +//! ``` use anyhow::Result; diff --git a/examples/status-code-propagation/rust/src/propagate_status_code.rs b/examples/status-code-propagation/rust/src/propagate_status_code.rs index f78e6e3bde..7b49b13522 100644 --- a/examples/status-code-propagation/rust/src/propagate_status_code.rs +++ b/examples/status-code-propagation/rust/src/propagate_status_code.rs @@ -44,7 +44,7 @@ impl Plugin for PropagateStatusCode { // - check for the presence of a value for `status_codes` (first parameter) // update the value if present (second parameter) res.context - .upsert(&"status_code".to_string(), |status_code: u16| { + .upsert("status_code", |status_code: u16| { // return the status code with the highest priority for &code in all_status_codes.iter() { if code == response_status_code || code == status_code { @@ -210,7 +210,7 @@ mod tests { let context = router_request.context; // Insert several status codes which shall override the router response status context - .insert(&"status_code".to_string(), json!(500u16)) + .insert("status_code", json!(500u16)) .expect("couldn't insert status_code"); Ok(supergraph::Response::fake_builder() diff --git a/fuzz/subgraph/src/model.rs b/fuzz/subgraph/src/model.rs index ea8b5f3c7d..9ecc74a23b 100644 --- a/fuzz/subgraph/src/model.rs +++ b/fuzz/subgraph/src/model.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case)] -use std::sync::Once; +use std::sync::LazyLock; use async_graphql::Context; use async_graphql::Object; @@ -128,25 +128,21 @@ impl User { } fn users() -> &'static [User] { - static mut USERS: Vec = vec![]; - static INIT: Once = Once::new(); - unsafe { - INIT.call_once(|| { - USERS = vec![ - User { - id: "1".to_string(), - name: "Ada Lovelace".to_string(), - username: "@ada".to_string(), - }, - User { - id: "2".to_string(), - name: "Alan Turing".to_string(), - username: "@complete".to_string(), - }, - ]; - }); - &USERS - } + static USERS: LazyLock> = LazyLock::new(|| { + vec![ + User { + id: "1".to_string(), + name: "Ada Lovelace".to_string(), + username: "@ada".to_string(), + }, + User { + id: "2".to_string(), + name: "Alan Turing".to_string(), + username: "@complete".to_string(), + }, + ] + }); + &USERS } /* @@ -223,43 +219,39 @@ impl Product { } fn products() -> &'static [Product] { - static mut PRODUCTS: Vec = vec![]; - static INIT: Once = Once::new(); - unsafe { - INIT.call_once(|| { - PRODUCTS = vec![ - Product { - upc: "1".to_string(), - name: Some("Table".to_string()), - price: 899, - weight: 100, - inStock: true, - }, - Product { - upc: "2".to_string(), - name: Some("Couch".to_string()), - price: 1299, - weight: 1000, - inStock: false, - }, - Product { - upc: "3".to_string(), - name: Some("Chair".to_string()), - price: 54, - weight: 50, - inStock: true, - }, - Product { - upc: "4".to_string(), - name: Some("Bed".to_string()), - price: 1000, - weight: 1200, - inStock: false, - }, - ]; - }); - &PRODUCTS - } + static PRODUCTS: LazyLock> = LazyLock::new(|| { + vec![ + Product { + upc: "1".to_string(), + name: Some("Table".to_string()), + price: 899, + weight: 100, + inStock: true, + }, + Product { + upc: "2".to_string(), + name: Some("Couch".to_string()), + price: 1299, + weight: 1000, + inStock: false, + }, + Product { + upc: "3".to_string(), + name: Some("Chair".to_string()), + price: 54, + weight: 50, + inStock: true, + }, + Product { + upc: "4".to_string(), + name: Some("Bed".to_string()), + price: 1000, + weight: 1200, + inStock: false, + }, + ] + }); + &PRODUCTS } /* @@ -303,37 +295,33 @@ impl Review { } fn reviews() -> &'static [Review] { - static mut REVIEWS: Vec = vec![]; - static INIT: Once = Once::new(); - unsafe { - INIT.call_once(|| { - REVIEWS = vec![ - Review { - id: "1".to_string(), - authorId: "1".to_string(), - productUpc: "1".to_string(), - body: Some("Love it!".to_string()), - }, - Review { - id: "2".to_string(), - authorId: "1".to_string(), - productUpc: "2".to_string(), - body: Some("Too expensive.".to_string()), - }, - Review { - id: "3".to_string(), - authorId: "2".to_string(), - productUpc: "3".to_string(), - body: Some("Could be better.".to_string()), - }, - Review { - id: "4".to_string(), - authorId: "2".to_string(), - productUpc: "1".to_string(), - body: Some("Prefer something else.".to_string()), - }, - ]; - }); - &REVIEWS - } + static REVIEWS: LazyLock> = LazyLock::new(|| { + vec![ + Review { + id: "1".to_string(), + authorId: "1".to_string(), + productUpc: "1".to_string(), + body: Some("Love it!".to_string()), + }, + Review { + id: "2".to_string(), + authorId: "1".to_string(), + productUpc: "2".to_string(), + body: Some("Too expensive.".to_string()), + }, + Review { + id: "3".to_string(), + authorId: "2".to_string(), + productUpc: "3".to_string(), + body: Some("Could be better.".to_string()), + }, + Review { + id: "4".to_string(), + authorId: "2".to_string(), + productUpc: "1".to_string(), + body: Some("Prefer something else.".to_string()), + }, + ] + }); + &REVIEWS } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 0c7dc7c811..a92784a00a 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] # renovate-automation: rustc version -channel = "1.76.0" # If updated, remove `rowan` dependency from apollo-router/Cargo.toml -components = [ "rustfmt", "clippy" ] +channel = "1.83.0" +components = ["rustfmt", "clippy"] From 5038401f6a977185024b7fbe981e3778facff303 Mon Sep 17 00:00:00 2001 From: Edward Huang Date: Thu, 12 Dec 2024 10:03:18 -0800 Subject: [PATCH 18/35] docs: endpoint path param and wildcard (#6442) --- docs/source/reference/router/configuration.mdx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/source/reference/router/configuration.mdx b/docs/source/reference/router/configuration.mdx index 4b6747f7af..9a84e711bb 100644 --- a/docs/source/reference/router/configuration.mdx +++ b/docs/source/reference/router/configuration.mdx @@ -477,15 +477,19 @@ supergraph: The path must start with `/`. -Path parameters and wildcards are supported. For example: +A path can contain parameters and wildcards: -- `/:my_dynamic_prefix/graphql` matches both `/my_project_a/graphql` and `/my_project_b/graphql`. -- `/graphql/*` matches `/graphql/my_project_a` and `/graphql/my_project_b`. -- `/g*` matches `/graphql`, `/gateway` and `/graphql/api`. +- `/:parameter` matches a single segment. For example: + - `/abc/:wildcard/def` matches `/abc/1/def` and `/abc/whatever/def`, but it doesn't match `/abc/1/2/def` or `/abc/def` + +- `/*parameter` matches all segments in the rest of a path. For example: + - `/abc/*wildcard` matches `/abc/1/def` and `/abc/w/h/a/t/e/v/e/r`, but it doesn't match `/abc/` or `/not_abc_at_all` -The router does _not_ support wildcards in the _middle_ of a path (e.g., `/*/graphql`). Instead, use a path parameter (e.g., `/:parameter/graphql`). +- Both `:` and `*` syntaxes require a name, even though you can’t use those names anywhere. + +- The router doesn't support wildcards in the _middle_ of a path (e.g., `/*/graphql`). Instead, use a path parameter (e.g., `/:parameter/graphql`). From efd912a969092c3507dfa40bce5f94318d37ff25 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Thu, 12 Dec 2024 19:07:50 +0100 Subject: [PATCH 19/35] docs: cmake and nodejs are no longer required (#6448) --- .circleci/config.yml | 34 +------------------ DEVELOPMENT.md | 9 ++--- dockerfiles/diy/dockerfiles/Dockerfile.repo | 3 +- .../routing/customization/custom-binary.mdx | 15 +++----- xtask/README.md | 2 +- 5 files changed, 13 insertions(+), 50 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 051f824bd0..220c76f0de 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -82,10 +82,6 @@ parameters: protoc_version: type: string default: "21.8" - # note the cmake version is only used for manual installs, not for installs from a package manager like apt or homebrew - cmake_version: - type: string - default: "3.31.1" nightly: type: boolean default: false @@ -237,37 +233,9 @@ commands: command: | if [[ ! -d "$HOME/.deb" ]]; then mkdir $HOME/.deb - sudo apt-get --download-only -o Dir::Cache="$HOME/.deb" -o Dir::Cache::archives="$HOME/.deb" install libssl-dev libdw-dev cmake + sudo apt-get --download-only -o Dir::Cache="$HOME/.deb" -o Dir::Cache::archives="$HOME/.deb" install libssl-dev libdw-dev fi sudo dpkg -i $HOME/.deb/*.deb - - when: - condition: - or: - - equal: [ *windows_build_executor, << parameters.platform >> ] - - equal: [ *windows_test_executor, << parameters.platform >> ] - steps: - - run: - name: Install CMake - command: | - mkdir -p "$HOME/.local" - if [[ ! -f "$HOME/.local/bin/cmake" ]]; then - curl -L https://github.com/Kitware/CMake/releases/download/v<< pipeline.parameters.cmake_version >>/cmake-<< pipeline.parameters.cmake_version >>-windows-x86_64.zip --output cmake.zip - # The zip file has a root directory, so we put it somewhere else first before placing the files in .local - unzip cmake.zip -d /tmp > /dev/null - cp /tmp/cmake-<< pipeline.parameters.cmake_version >>-windows-x86_64/* -R "$HOME/.local" - fi - - cmake --version - - when: - condition: - or: - - equal: [ *macos_build_executor, << parameters.platform >> ] - - equal: [ *macos_test_executor, << parameters.platform >> ] - steps: - - run: - name: Install CMake - command: | - brew install cmake install_protoc: parameters: platform: diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index c46019ff93..ce2efbb4a5 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -18,16 +18,17 @@ The **Apollo Router Core** is a configurable, high-performance **graph router** ## Development -You will need a recent version of rust (`1.72` works well as of writing). +You will need a recent version of rust (`1.83.0` works well as of writing). Installing rust [using rustup](https://www.rust-lang.org/tools/install) is -the recommended way to do it as it will install rustup, rustfmt and other -goodies that are not always included by default in other rust distribution channels: +the recommended way to do it as it will automatically install the toolchain version +specified in `rust-toolchain.toml`, including rustfmt and clippy +that are not always included by default in other rust distribution channels: ``` curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` -In addition, you will need to [install protoc](https://grpc.io/docs/protoc-installation/) and [cmake](https://cmake.org/). +In addition, you will need to [install protoc](https://grpc.io/docs/protoc-installation/). Set up your git hooks: diff --git a/dockerfiles/diy/dockerfiles/Dockerfile.repo b/dockerfiles/diy/dockerfiles/Dockerfile.repo index cabfed825c..c0d9211488 100644 --- a/dockerfiles/diy/dockerfiles/Dockerfile.repo +++ b/dockerfiles/diy/dockerfiles/Dockerfile.repo @@ -8,8 +8,7 @@ WORKDIR /usr/src/router # Update our build image and install required packages RUN apt-get update RUN apt-get -y install \ - protobuf-compiler \ - cmake + protobuf-compiler # Add rustfmt since build requires it RUN rustup component add rustfmt diff --git a/docs/source/routing/customization/custom-binary.mdx b/docs/source/routing/customization/custom-binary.mdx index 8f2dbf1bbe..1446a57930 100644 --- a/docs/source/routing/customization/custom-binary.mdx +++ b/docs/source/routing/customization/custom-binary.mdx @@ -15,29 +15,24 @@ Learn how to compile a custom binary from Apollo Router Core source, which is re ## Prerequisites -To compile the router, you need to have the following installed: - -* [Rust 1.83.0 or later](https://www.rust-lang.org/tools/install) -* [Node.js 16.9.1 or later](https://nodejs.org/en/download/) -* [CMake 3.5.1 or later](https://cmake.org/download/) +To compile the router, you need to have [Rust 1.83.0 or later](https://www.rust-lang.org/tools/install) installed. -After you install the above, also install the `cargo-xtask` and `cargo-scaffold` crates: +After you install the above, also install the `cargo-scaffold` tool: ```sh -cargo install cargo-xtask cargo install cargo-scaffold ``` ## 1. Create a new project -1. Use the `cargo-scaffold` command to create a project for your custom router: +1. Use the `cargo scaffold` command to create a project for your custom router: ```bash - cargo-scaffold scaffold https://github.com/apollographql/router.git -r apollo-router-scaffold/templates/base -t main + cargo scaffold https://github.com/apollographql/router.git -r apollo-router-scaffold/templates/base -t main ``` -2. The `cargo-scaffold` command prompts you for some configuration settings. For the purposes of this tutorial, set your project's name to `starstuff`. +2. The `cargo scaffold` command prompts you for some configuration settings. For the purposes of this tutorial, set your project's name to `starstuff`. 3. After your project is created, change to the `starstuff` directory: diff --git a/xtask/README.md b/xtask/README.md index 0bd6883a87..e1af5e6bc1 100644 --- a/xtask/README.md +++ b/xtask/README.md @@ -1,6 +1,6 @@ # xtask -The Apollo Router project uses [xtask](https://github.com/matklad/cargo-xtask) to help with the automation of code quality. +The Apollo Router project uses the [xtask](https://github.com/matklad/cargo-xtask) pattern to help with the automation of code quality. You can run `cargo xtask --help` to see the usage. Generally we recommend that you continue to use the default cargo commands like `cargo fmt`, `cargo clippy`, and `cargo test`, but if you are debugging something that is happening in CI it can be useful to run the xtask commands that we run [in CI](../.github/workflows). From 4d5b6ad73252503bdd1e9faeb74ee48604392102 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Thu, 12 Dec 2024 20:12:23 +0200 Subject: [PATCH 20/35] Truncating Invalid Error Paths (#6359) --- .changesets/fix_fix_invalid_error_path.md | 10 ++ ..._tests__unauthenticated_request_defer.snap | 14 +- .../src/services/execution/service.rs | 17 +++ ...h_invalid_paths_on_query_with_defer-2.snap | 30 ++++ ...ith_invalid_paths_on_query_with_defer.snap | 27 ++++ .../src/services/supergraph/tests.rs | 142 ++++++++++++++++-- apollo-router/src/spec/query.rs | 16 +- apollo-router/src/spec/selection.rs | 74 +++++---- .../tests/integration/subgraph_response.rs | 114 ++++++++++++++ 9 files changed, 402 insertions(+), 42 deletions(-) create mode 100644 .changesets/fix_fix_invalid_error_path.md create mode 100644 apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_with_invalid_paths_on_query_with_defer-2.snap create mode 100644 apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_with_invalid_paths_on_query_with_defer.snap diff --git a/.changesets/fix_fix_invalid_error_path.md b/.changesets/fix_fix_invalid_error_path.md new file mode 100644 index 0000000000..339fb5110d --- /dev/null +++ b/.changesets/fix_fix_invalid_error_path.md @@ -0,0 +1,10 @@ +### Truncating Invalid Error Paths ([PR #6359](https://github.com/apollographql/router/pull/6359)) + +This fix addresses an issue where the router was silently dropping subgraph errors that included invalid paths. + +According to the [GraphQL Specification](https://spec.graphql.org/draft/#sel-GAPHRPHCAACCpC8-T) an error path must point to a **response field**: +> If an error can be associated to a particular field in the GraphQL result, it must contain an entry with the key path that details the path of the response field which experienced the error. + +If a subgraph error includes a path that can't be matched to a response field, the router now truncates the path to the nearest valid field path. + +By [@IvanGoncharov](https://github.com/IvanGoncharov) in https://github.com/apollographql/router/pull/6359 diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__unauthenticated_request_defer.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__unauthenticated_request_defer.snap index 834762190d..561696a5f7 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__unauthenticated_request_defer.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__unauthenticated_request_defer.snap @@ -1,6 +1,7 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs expression: first_response +snapshot_kind: text --- { "data": { @@ -10,5 +11,16 @@ expression: first_response "id": 0 } } - } + }, + "errors": [ + { + "message": "Unauthorized field or type", + "path": [ + "orga" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + } + ] } diff --git a/apollo-router/src/services/execution/service.rs b/apollo-router/src/services/execution/service.rs index 2869f1ade9..de6057f287 100644 --- a/apollo-router/src/services/execution/service.rs +++ b/apollo-router/src/services/execution/service.rs @@ -344,6 +344,23 @@ impl ExecutionService { , ); + for error in response.errors.iter_mut() { + if let Some(path) = &mut error.path { + // Check if path can be matched to the supergraph query and truncate if not + let matching_len = query.matching_error_path_length(path); + if path.len() != matching_len { + path.0.drain(matching_len..); + + if path.is_empty() { + error.path = None; + } + + // if path was invalid that means we can't trust locations either + error.locations.clear(); + } + } + } + nullified_paths.extend(paths); let mut referenced_enums = context diff --git a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_with_invalid_paths_on_query_with_defer-2.snap b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_with_invalid_paths_on_query_with_defer-2.snap new file mode 100644 index 0000000000..ab3e954854 --- /dev/null +++ b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_with_invalid_paths_on_query_with_defer-2.snap @@ -0,0 +1,30 @@ +--- +source: apollo-router/src/services/supergraph/tests.rs +expression: stream.next_response().await.unwrap() +snapshot_kind: text +--- +{ + "hasNext": false, + "incremental": [ + { + "data": { + "errorField": null + }, + "path": [ + "computer" + ], + "errors": [ + { + "message": "Error field", + "path": [ + "computer", + "errorField" + ], + "extensions": { + "service": "computers" + } + } + ] + } + ] +} diff --git a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_with_invalid_paths_on_query_with_defer.snap b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_with_invalid_paths_on_query_with_defer.snap new file mode 100644 index 0000000000..cf02e6c91f --- /dev/null +++ b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_with_invalid_paths_on_query_with_defer.snap @@ -0,0 +1,27 @@ +--- +source: apollo-router/src/services/supergraph/tests.rs +expression: stream.next_response().await.unwrap() +snapshot_kind: text +--- +{ + "data": { + "computer": { + "id": "Computer1" + } + }, + "errors": [ + { + "message": "Subgraph error with invalid path", + "extensions": { + "service": "computers" + } + }, + { + "message": "Subgraph error with partially valid path", + "extensions": { + "service": "computers" + } + } + ], + "hasNext": true +} diff --git a/apollo-router/src/services/supergraph/tests.rs b/apollo-router/src/services/supergraph/tests.rs index 059d7baa09..cee234761d 100644 --- a/apollo-router/src/services/supergraph/tests.rs +++ b/apollo-router/src/services/supergraph/tests.rs @@ -533,16 +533,15 @@ async fn errors_from_primary_on_deferred_responses() { computer(id: ID!): Computer }"#; - let subgraphs = MockedSubgraphs([ - ("computers", MockSubgraph::builder().with_json( - serde_json::json!{{"query":"{currentUser{__typename id}}"}}, - serde_json::json!{{"data": {"currentUser": { "__typename": "User", "id": "0" }}}} - ) - .with_json( - serde_json::json!{{ - "query":"{computer(id:\"Computer1\"){id errorField}}", - }}, - serde_json::json!{{ + let subgraphs = MockedSubgraphs( + [( + "computers", + MockSubgraph::builder() + .with_json( + serde_json::json! {{ + "query":"{computer(id:\"Computer1\"){id errorField}}", + }}, + serde_json::json! {{ "data": { "computer": { "id": "Computer1" @@ -560,9 +559,126 @@ async fn errors_from_primary_on_deferred_responses() { "path": ["computer","errorField"], } ] - }} - ).build()), - ].into_iter().collect()); + }}, + ) + .build(), + )] + .into_iter() + .collect(), + ); + + let service = TestHarness::builder() + .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .unwrap() + .schema(schema) + .extra_plugin(subgraphs) + .build_supergraph() + .await + .unwrap(); + + let request = supergraph::Request::fake_builder() + .context(defer_context()) + .query( + r#"query { + computer(id: "Computer1") { + id + ...ComputerErrorField @defer + } + } + fragment ComputerErrorField on Computer { + errorField + }"#, + ) + .build() + .unwrap(); + + let mut stream = service.oneshot(request).await.unwrap(); + + insta::assert_json_snapshot!(stream.next_response().await.unwrap()); + + insta::assert_json_snapshot!(stream.next_response().await.unwrap()); +} + +#[tokio::test] +async fn errors_with_invalid_paths_on_query_with_defer() { + let schema = r#" + 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) 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 link__Import + enum link__Purpose { + SECURITY + EXECUTION + } + + type Computer + @join__type(graph: COMPUTERS) + { + id: ID! + errorField: String + nonNullErrorField: String! + } + + scalar join__FieldSet + + enum join__Graph { + COMPUTERS @join__graph(name: "computers", url: "http://localhost:4001/") + } + + + type Query + @join__type(graph: COMPUTERS) + { + computer(id: ID!): Computer + }"#; + + let subgraphs = MockedSubgraphs( + [( + "computers", + MockSubgraph::builder() + .with_json( + serde_json::json! {{ + "query":"{computer(id:\"Computer1\"){id errorField}}", + }}, + serde_json::json! {{ + "data": { + "computer": { + "id": "Computer1" + } + }, + "errors": [ + { + "message": "Subgraph error with invalid path", + "path": ["invalid","path"], + }, + { + "message": "Subgraph error with partially valid path", + "path": ["currentUser", "invalidSubpath"], + }, + { + "message": "Error field", + "path": ["computer","errorField", "errorFieldSubpath"], + } + ] + }}, + ) + .build(), + )] + .into_iter() + .collect(), + ); let service = TestHarness::builder() .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) diff --git a/apollo-router/src/spec/query.rs b/apollo-router/src/spec/query.rs index 460f8cac3d..4f83d7c8e3 100644 --- a/apollo-router/src/spec/query.rs +++ b/apollo-router/src/spec/query.rs @@ -983,9 +983,21 @@ impl Query { Some(subselection) => &subselection.selection_set, None => &self.operation.selection_set, }; - selection_set + let match_length = selection_set .iter() - .any(|selection| selection.contains_error_path(&path.0, &self.fragments)) + .map(|selection| selection.matching_error_path_length(&path.0, &self.fragments)) + .max() + .unwrap_or(0); + path.len() == match_length + } + + pub(crate) fn matching_error_path_length(&self, path: &Path) -> usize { + self.operation + .selection_set + .iter() + .map(|selection| selection.matching_error_path_length(&path.0, &self.fragments)) + .max() + .unwrap_or(0) } pub(crate) fn defer_variables_set(&self, variables: &Object) -> BooleanValues { diff --git a/apollo-router/src/spec/selection.rs b/apollo-router/src/spec/selection.rs index 4e093ea4fb..0370e5d2df 100644 --- a/apollo-router/src/spec/selection.rs +++ b/apollo-router/src/spec/selection.rs @@ -189,11 +189,15 @@ impl Selection { }) } - pub(crate) fn contains_error_path(&self, path: &[PathElement], fragments: &Fragments) -> bool { - match (path.first(), self) { - (None, _) => true, + pub(crate) fn matching_error_path_length( + &self, + path: &[PathElement], + fragments: &Fragments, + ) -> usize { + match (path.split_first(), self) { + (None, _) => 0, ( - Some(PathElement::Key(key, _)), + Some((PathElement::Key(key, _), rest)), Selection::Field { name, alias, @@ -204,17 +208,23 @@ impl Selection { if alias.as_ref().unwrap_or(name).as_str() == key.as_str() { match selection_set { // if we don't select after that field, the path should stop there - None => path.len() == 1, - Some(set) => set - .iter() - .any(|selection| selection.contains_error_path(&path[1..], fragments)), + None => 1, + Some(set) => { + set.iter() + .map(|selection| { + selection.matching_error_path_length(rest, fragments) + }) + .max() + .unwrap_or(0) + + 1 + } } } else { - false + 0 } } ( - Some(PathElement::Fragment(fragment)), + Some((PathElement::Fragment(fragment), rest)), Selection::InlineFragment { type_condition, selection_set, @@ -224,42 +234,54 @@ impl Selection { if fragment.as_str() == type_condition.as_str() { selection_set .iter() - .any(|selection| selection.contains_error_path(&path[1..], fragments)) + .map(|selection| selection.matching_error_path_length(rest, fragments)) + .max() + .unwrap_or(0) + + 1 } else { - false + 0 } } - (Some(PathElement::Fragment(fragment)), Self::FragmentSpread { name, .. }) => { + (Some((PathElement::Fragment(fragment), rest)), Self::FragmentSpread { name, .. }) => { if let Some(f) = fragments.get(name) { if fragment.as_str() == f.type_condition.as_str() { f.selection_set .iter() - .any(|selection| selection.contains_error_path(&path[1..], fragments)) + .map(|selection| selection.matching_error_path_length(rest, fragments)) + .max() + .unwrap_or(0) + + 1 } else { - false + 0 } } else { - false + 0 } } (_, Self::FragmentSpread { name, .. }) => { if let Some(f) = fragments.get(name) { f.selection_set .iter() - .any(|selection| selection.contains_error_path(path, fragments)) + .map(|selection| selection.matching_error_path_length(path, fragments)) + .max() + .unwrap_or(0) } else { - false + 0 } } - (Some(PathElement::Index(_)), _) | (Some(PathElement::Flatten(_)), _) => { - self.contains_error_path(&path[1..], fragments) + (Some((PathElement::Index(_), rest)), _) + | (Some((PathElement::Flatten(_), rest)), _) => { + self.matching_error_path_length(rest, fragments) + 1 } - (Some(PathElement::Key(_, _)), Selection::InlineFragment { selection_set, .. }) => { - selection_set - .iter() - .any(|selection| selection.contains_error_path(path, fragments)) - } - (Some(PathElement::Fragment(_)), Selection::Field { .. }) => false, + ( + Some((PathElement::Key(_, _), _)), + Selection::InlineFragment { selection_set, .. }, + ) => selection_set + .iter() + .map(|selection| selection.matching_error_path_length(path, fragments)) + .max() + .unwrap_or(0), + (Some((PathElement::Fragment(_), _)), Selection::Field { .. }) => 0, } } } diff --git a/apollo-router/tests/integration/subgraph_response.rs b/apollo-router/tests/integration/subgraph_response.rs index e37a0da067..5272b74ace 100644 --- a/apollo-router/tests/integration/subgraph_response.rs +++ b/apollo-router/tests/integration/subgraph_response.rs @@ -409,3 +409,117 @@ async fn test_invalid_error_locations_contains_negative_one_location() -> Result router.graceful_shutdown().await; Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_valid_error_path() -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config(CONFIG) + .responder(ResponseTemplate::new(200).set_body_json(json!({ + "data": { "topProducts": null }, + "errors": [{ + "message": "Some error on subgraph", + "path": ["topProducts"] + }] + }))) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let (_trace_id, response) = router + .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .await; + assert_eq!(response.status(), 200); + assert_eq!( + response.json::().await?, + json!({ + "data": { "topProducts": null }, + "errors": [{ + "message":"Some error on subgraph", + "path":["topProducts"], + "extensions": { "service": "products" } + }] + }) + ); + + router.graceful_shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_invalid_error_path() -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config(CONFIG) + .responder(ResponseTemplate::new(200).set_body_json(json!({ + "data": { "topProducts": null }, + "errors": [{ + "message": "Some error on subgraph", + "path": ["some","path", 42] + }] + }))) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let (_trace_id, response) = router + .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .await; + assert_eq!(response.status(), 200); + assert_eq!( + response.json::().await?, + json!({ + "data": { "topProducts": null }, + "errors": [{ + "message":"Some error on subgraph", + "extensions": { + "service": "products" + } + }] + }) + ); + + router.graceful_shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_partially_valid_error_path() -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config(CONFIG) + .responder(ResponseTemplate::new(200).set_body_json(json!({ + "data": { "topProducts": null }, + "errors": [{ + "message": "Some error on subgraph", + "path": ["topProducts","invalid", 42] + }] + }))) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let (_trace_id, response) = router + .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .await; + assert_eq!(response.status(), 200); + assert_eq!( + response.json::().await?, + json!({ + "data": { "topProducts": null }, + "errors": [{ + "message": "Some error on subgraph", + "path": ["topProducts"], + "extensions": { + "service": "products" + } + }] + }) + ); + + router.graceful_shutdown().await; + Ok(()) +} From e6b2fd2f43fd43ca7005b5bf6f0b793d101e73bc Mon Sep 17 00:00:00 2001 From: Tyler Bloom Date: Thu, 12 Dec 2024 20:08:01 -0500 Subject: [PATCH 21/35] chore(deps): Removed lazy_static (#6449) --- Cargo.lock | 2 - apollo-federation/Cargo.toml | 1 - apollo-federation/src/error/mod.rs | 613 +++++++++++------- .../src/link/context_spec_definition.rs | 10 +- .../src/link/cost_spec_definition.rs | 9 +- .../src/link/federation_spec_definition.rs | 10 +- .../src/link/inaccessible_spec_definition.rs | 9 +- .../src/link/join_spec_definition.rs | 10 +- .../src/link/link_spec_definition.rs | 14 +- .../src/query_graph/build_query_graph.rs | 13 +- .../schema/argument_composition_strategies.rs | 25 +- apollo-federation/src/subgraph/spec.rs | 9 +- apollo-federation/src/supergraph/mod.rs | 9 +- .../templates/base/rust-toolchain.toml | 2 +- apollo-router/Cargo.toml | 3 +- apollo-router/src/error.rs | 2 +- apollo-router/src/plugins/telemetry/otlp.rs | 10 +- .../src/plugins/telemetry/tracing/jaeger.rs | 8 +- .../src/plugins/telemetry/tracing/zipkin.rs | 8 +- 19 files changed, 463 insertions(+), 304 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1fb43a1848..b9f77828ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,7 +214,6 @@ dependencies = [ "indexmap 2.2.6", "insta", "itertools 0.13.0", - "lazy_static", "multimap 0.10.0", "nom", "petgraph", @@ -322,7 +321,6 @@ dependencies = [ "jsonpath_lib", "jsonschema", "jsonwebtoken", - "lazy_static", "libc", "libtest-mimic", "linkme", diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index 739d379f60..e417a998ad 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -25,7 +25,6 @@ derive_more = "0.99.17" hashbrown = "0.15.1" indexmap = { version = "2.2.6", features = ["serde"] } itertools = "0.13.0" -lazy_static = "1.4.0" multimap = "0.10.0" nom = "7.1.3" petgraph = { version = "0.6.4", features = ["serde-1"] } diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index caeecf0a4a..ebafcbbdd6 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -2,12 +2,12 @@ use std::cmp::Ordering; use std::fmt::Display; use std::fmt::Formatter; use std::fmt::Write; +use std::sync::LazyLock; use apollo_compiler::validation::DiagnosticList; use apollo_compiler::validation::WithErrors; use apollo_compiler::InvalidNameError; use apollo_compiler::Name; -use lazy_static::lazy_static; use crate::subgraph::spec::FederationSpecError; @@ -740,118 +740,154 @@ impl ErrorCodeCategory { } } -lazy_static! { - static ref INVALID_GRAPHQL: ErrorCodeDefinition = ErrorCodeDefinition::new( +static INVALID_GRAPHQL: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "INVALID_GRAPHQL".to_owned(), "A schema is invalid GraphQL: it violates one of the rule of the specification.".to_owned(), None, - ); - - static ref DIRECTIVE_DEFINITION_INVALID: ErrorCodeDefinition = ErrorCodeDefinition::new( + ) +}); +static DIRECTIVE_DEFINITION_INVALID: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "DIRECTIVE_DEFINITION_INVALID".to_owned(), "A built-in or federation directive has an invalid definition in the schema.".to_owned(), Some(ErrorCodeMetadata { replaces: &["TAG_DEFINITION_INVALID"], ..DEFAULT_METADATA.clone() }), - ); + ) +}); - static ref TYPE_DEFINITION_INVALID: ErrorCodeDefinition = ErrorCodeDefinition::new( +static TYPE_DEFINITION_INVALID: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "TYPE_DEFINITION_INVALID".to_owned(), "A built-in or federation type has an invalid definition in the schema.".to_owned(), None, - ); + ) +}); - static ref UNSUPPORTED_LINKED_FEATURE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static UNSUPPORTED_LINKED_FEATURE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "UNSUPPORTED_LINKED_FEATURE".to_owned(), "Indicates that a feature used in a @link is either unsupported or is used with unsupported options.".to_owned(), None, - ); + ) +}); - static ref UNKNOWN_FEDERATION_LINK_VERSION: ErrorCodeDefinition = ErrorCodeDefinition::new( +static UNKNOWN_FEDERATION_LINK_VERSION: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "UNKNOWN_FEDERATION_LINK_VERSION".to_owned(), "The version of federation in a @link directive on the schema is unknown.".to_owned(), None, - ); + ) +}); - static ref UNKNOWN_LINK_VERSION: ErrorCodeDefinition = ErrorCodeDefinition::new( +static UNKNOWN_LINK_VERSION: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "UNKNOWN_LINK_VERSION".to_owned(), "The version of @link set on the schema is unknown.".to_owned(), Some(ErrorCodeMetadata { added_in: "2.1.0", replaces: &[], }), - ); + ) +}); - static ref FIELDS_HAS_ARGS: ErrorCodeCategory = ErrorCodeCategory::new_federation_directive( +static FIELDS_HAS_ARGS: LazyLock> = LazyLock::new(|| { + ErrorCodeCategory::new_federation_directive( "FIELDS_HAS_ARGS".to_owned(), - Box::new(|directive| format!("The `fields` argument of a `@{}` directive includes a field defined with arguments (which is not currently supported).", directive)), - None, - ); - - static ref KEY_FIELDS_HAS_ARGS: ErrorCodeDefinition = FIELDS_HAS_ARGS.create_code("key".to_owned()); - static ref PROVIDES_FIELDS_HAS_ARGS: ErrorCodeDefinition = FIELDS_HAS_ARGS.create_code("provides".to_owned()); - - static ref DIRECTIVE_FIELDS_MISSING_EXTERNAL: ErrorCodeCategory = ErrorCodeCategory::new_federation_directive( - "FIELDS_MISSING_EXTERNAL".to_owned(), - Box::new(|directive| format!("The `fields` argument of a `@{}` directive includes a field that is not marked as `@external`.", directive)), - Some(ErrorCodeMetadata { - added_in: FED1_CODE, - replaces: &[], - }), - ); - - static ref PROVIDES_FIELDS_MISSING_EXTERNAL: ErrorCodeDefinition = - DIRECTIVE_FIELDS_MISSING_EXTERNAL.create_code("provides".to_owned()); - static ref REQUIRES_FIELDS_MISSING_EXTERNAL: ErrorCodeDefinition = - DIRECTIVE_FIELDS_MISSING_EXTERNAL.create_code("requires".to_owned()); - - static ref DIRECTIVE_UNSUPPORTED_ON_INTERFACE: ErrorCodeCategory = ErrorCodeCategory::new_federation_directive( - "UNSUPPORTED_ON_INTERFACE".to_owned(), Box::new(|directive| { - let suffix = if directive == "key" { - "only supported when @linking to federation 2.3+" - } else { - "not (yet) supported" - }; - format!( - "A `@{}` directive is used on an interface, which is {}.", - directive, suffix - ) + format!("The `fields` argument of a `@{}` directive includes a field defined with arguments (which is not currently supported).", directive) }), None, - ); + ) +}); + +static KEY_FIELDS_HAS_ARGS: LazyLock = + LazyLock::new(|| FIELDS_HAS_ARGS.create_code("key".to_owned())); + +static PROVIDES_FIELDS_HAS_ARGS: LazyLock = + LazyLock::new(|| FIELDS_HAS_ARGS.create_code("provides".to_owned())); + +static DIRECTIVE_FIELDS_MISSING_EXTERNAL: LazyLock> = LazyLock::new( + || { + ErrorCodeCategory::new_federation_directive( + "FIELDS_MISSING_EXTERNAL".to_owned(), + Box::new(|directive| { + format!("The `fields` argument of a `@{}` directive includes a field that is not marked as `@external`.", directive) + }), + Some(ErrorCodeMetadata { + added_in: FED1_CODE, + replaces: &[], + }), + ) + }, +); + +static PROVIDES_FIELDS_MISSING_EXTERNAL: LazyLock = + LazyLock::new(|| DIRECTIVE_FIELDS_MISSING_EXTERNAL.create_code("provides".to_owned())); +static REQUIRES_FIELDS_MISSING_EXTERNAL: LazyLock = + LazyLock::new(|| DIRECTIVE_FIELDS_MISSING_EXTERNAL.create_code("requires".to_owned())); + +static DIRECTIVE_UNSUPPORTED_ON_INTERFACE: LazyLock> = + LazyLock::new(|| { + ErrorCodeCategory::new_federation_directive( + "UNSUPPORTED_ON_INTERFACE".to_owned(), + Box::new(|directive| { + let suffix = if directive == "key" { + "only supported when @linking to federation 2.3+" + } else { + "not (yet) supported" + }; + format!( + "A `@{}` directive is used on an interface, which is {}.", + directive, suffix + ) + }), + None, + ) + }); - static ref KEY_UNSUPPORTED_ON_INTERFACE: ErrorCodeDefinition = - DIRECTIVE_UNSUPPORTED_ON_INTERFACE.create_code("key".to_owned()); - static ref PROVIDES_UNSUPPORTED_ON_INTERFACE: ErrorCodeDefinition = - DIRECTIVE_UNSUPPORTED_ON_INTERFACE.create_code("provides".to_owned()); - static ref REQUIRES_UNSUPPORTED_ON_INTERFACE: ErrorCodeDefinition = - DIRECTIVE_UNSUPPORTED_ON_INTERFACE.create_code("requires".to_owned()); +static KEY_UNSUPPORTED_ON_INTERFACE: LazyLock = + LazyLock::new(|| DIRECTIVE_UNSUPPORTED_ON_INTERFACE.create_code("key".to_owned())); +static PROVIDES_UNSUPPORTED_ON_INTERFACE: LazyLock = + LazyLock::new(|| DIRECTIVE_UNSUPPORTED_ON_INTERFACE.create_code("provides".to_owned())); +static REQUIRES_UNSUPPORTED_ON_INTERFACE: LazyLock = + LazyLock::new(|| DIRECTIVE_UNSUPPORTED_ON_INTERFACE.create_code("requires".to_owned())); - static ref DIRECTIVE_IN_FIELDS_ARG: ErrorCodeCategory = ErrorCodeCategory::new_federation_directive( +static DIRECTIVE_IN_FIELDS_ARG: LazyLock> = LazyLock::new(|| { + ErrorCodeCategory::new_federation_directive( "DIRECTIVE_IN_FIELDS_ARG".to_owned(), - Box::new(|directive| format!("The `fields` argument of a `@{}` directive includes some directive applications. This is not supported", directive)), + Box::new(|directive| { + format!("The `fields` argument of a `@{}` directive includes some directive applications. This is not supported", directive) + }), Some(ErrorCodeMetadata { added_in: "2.1.0", replaces: &[], }), - ); - - static ref KEY_DIRECTIVE_IN_FIELDS_ARGS: ErrorCodeDefinition = DIRECTIVE_IN_FIELDS_ARG.create_code("key".to_owned()); - static ref PROVIDES_DIRECTIVE_IN_FIELDS_ARGS: ErrorCodeDefinition = DIRECTIVE_IN_FIELDS_ARG.create_code("provides".to_owned()); - static ref REQUIRES_DIRECTIVE_IN_FIELDS_ARGS: ErrorCodeDefinition = DIRECTIVE_IN_FIELDS_ARG.create_code("requires".to_owned()); - - static ref EXTERNAL_UNUSED: ErrorCodeDefinition = ErrorCodeDefinition::new( + ) +}); + +static KEY_DIRECTIVE_IN_FIELDS_ARGS: LazyLock = + LazyLock::new(|| DIRECTIVE_IN_FIELDS_ARG.create_code("key".to_owned())); +static PROVIDES_DIRECTIVE_IN_FIELDS_ARGS: LazyLock = + LazyLock::new(|| DIRECTIVE_IN_FIELDS_ARG.create_code("provides".to_owned())); +static REQUIRES_DIRECTIVE_IN_FIELDS_ARGS: LazyLock = + LazyLock::new(|| DIRECTIVE_IN_FIELDS_ARG.create_code("requires".to_owned())); + +static EXTERNAL_UNUSED: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "EXTERNAL_UNUSED".to_owned(), "An `@external` field is not being used by any instance of `@key`, `@requires`, `@provides` or to satisfy an interface implementation.".to_owned(), Some(ErrorCodeMetadata { added_in: FED1_CODE, replaces: &[], }), - ); +) +}); - static ref TYPE_WITH_ONLY_UNUSED_EXTERNAL: ErrorCodeDefinition = ErrorCodeDefinition::new( +static TYPE_WITH_ONLY_UNUSED_EXTERNAL: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "TYPE_WITH_ONLY_UNUSED_EXTERNAL".to_owned(), [ "A federation 1 schema has a composite type comprised only of unused external fields.".to_owned(), @@ -859,50 +895,68 @@ lazy_static! { "But when federation 1 schema are automatically migrated to federation 2 ones, unused external fields are automatically removed, and in rare case this can leave a type empty. If that happens, an error with this code will be raised".to_owned() ].join(" "), None, - ); +) +}); - static ref PROVIDES_ON_NON_OBJECT_FIELD: ErrorCodeDefinition = ErrorCodeDefinition::new( +static PROVIDES_ON_NON_OBJECT_FIELD: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "PROVIDES_ON_NON_OBJECT_FIELD".to_owned(), - "A `@provides` directive is used to mark a field whose base type is not an object type.".to_owned(), + "A `@provides` directive is used to mark a field whose base type is not an object type." + .to_owned(), None, - ); + ) +}); - static ref DIRECTIVE_INVALID_FIELDS_TYPE: ErrorCodeCategory = ErrorCodeCategory::new_federation_directive( +static DIRECTIVE_INVALID_FIELDS_TYPE: LazyLock> = LazyLock::new(|| { + ErrorCodeCategory::new_federation_directive( "INVALID_FIELDS_TYPE".to_owned(), - Box::new(|directive| format!("The value passed to the `fields` argument of a `@{}` directive is not a string.", directive)), + Box::new(|directive| { + format!( + "The value passed to the `fields` argument of a `@{}` directive is not a string.", + directive + ) + }), None, - ); - - static ref KEY_INVALID_FIELDS_TYPE: ErrorCodeDefinition = - DIRECTIVE_INVALID_FIELDS_TYPE.create_code("key".to_owned()); - static ref PROVIDES_INVALID_FIELDS_TYPE: ErrorCodeDefinition = - DIRECTIVE_INVALID_FIELDS_TYPE.create_code("provides".to_owned()); - static ref REQUIRES_INVALID_FIELDS_TYPE: ErrorCodeDefinition = - DIRECTIVE_INVALID_FIELDS_TYPE.create_code("requires".to_owned()); - - static ref DIRECTIVE_INVALID_FIELDS: ErrorCodeCategory = ErrorCodeCategory::new_federation_directive( + ) +}); + +static KEY_INVALID_FIELDS_TYPE: LazyLock = + LazyLock::new(|| DIRECTIVE_INVALID_FIELDS_TYPE.create_code("key".to_owned())); +static PROVIDES_INVALID_FIELDS_TYPE: LazyLock = + LazyLock::new(|| DIRECTIVE_INVALID_FIELDS_TYPE.create_code("provides".to_owned())); +static REQUIRES_INVALID_FIELDS_TYPE: LazyLock = + LazyLock::new(|| DIRECTIVE_INVALID_FIELDS_TYPE.create_code("requires".to_owned())); + +static DIRECTIVE_INVALID_FIELDS: LazyLock> = LazyLock::new(|| { + ErrorCodeCategory::new_federation_directive( "INVALID_FIELDS".to_owned(), - Box::new(|directive| format!("The `fields` argument of a `@{}` directive is invalid (it has invalid syntax, includes unknown fields, ...).", directive)), + Box::new(|directive| { + format!("The `fields` argument of a `@{}` directive is invalid (it has invalid syntax, includes unknown fields, ...).", directive) + }), None, - ); - - static ref KEY_INVALID_FIELDS: ErrorCodeDefinition = - DIRECTIVE_INVALID_FIELDS.create_code("key".to_owned()); - static ref PROVIDES_INVALID_FIELDS: ErrorCodeDefinition = - DIRECTIVE_INVALID_FIELDS.create_code("provides".to_owned()); - static ref REQUIRES_INVALID_FIELDS: ErrorCodeDefinition = - DIRECTIVE_INVALID_FIELDS.create_code("requires".to_owned()); - - static ref KEY_FIELDS_SELECT_INVALID_TYPE: ErrorCodeDefinition = ErrorCodeDefinition::new( + ) +}); + +static KEY_INVALID_FIELDS: LazyLock = + LazyLock::new(|| DIRECTIVE_INVALID_FIELDS.create_code("key".to_owned())); +static PROVIDES_INVALID_FIELDS: LazyLock = + LazyLock::new(|| DIRECTIVE_INVALID_FIELDS.create_code("provides".to_owned())); +static REQUIRES_INVALID_FIELDS: LazyLock = + LazyLock::new(|| DIRECTIVE_INVALID_FIELDS.create_code("requires".to_owned())); + +static KEY_FIELDS_SELECT_INVALID_TYPE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "KEY_FIELDS_SELECT_INVALID_TYPE".to_owned(), "The `fields` argument of `@key` directive includes a field whose type is a list, interface, or union type. Fields of these types cannot be part of a `@key`".to_owned(), Some(ErrorCodeMetadata { added_in: FED1_CODE, replaces: &[], }), - ); +) +}); - static ref ROOT_TYPE_USED: ErrorCodeCategory = ErrorCodeCategory::new( +static ROOT_TYPE_USED: LazyLock> = LazyLock::new(|| { + ErrorCodeCategory::new( Box::new(|element| { let kind: String = element.into(); format!("ROOT_{}_USED", kind.to_uppercase()) @@ -914,371 +968,486 @@ lazy_static! { Some(ErrorCodeMetadata { added_in: FED1_CODE, replaces: &[], - }) - - ); - - static ref ROOT_QUERY_USED: ErrorCodeDefinition = ROOT_TYPE_USED.create_code(SchemaRootKind::Query); - static ref ROOT_MUTATION_USED: ErrorCodeDefinition = ROOT_TYPE_USED.create_code(SchemaRootKind::Mutation); - static ref ROOT_SUBSCRIPTION_USED: ErrorCodeDefinition = ROOT_TYPE_USED.create_code(SchemaRootKind::Subscription); - - static ref INVALID_SUBGRAPH_NAME: ErrorCodeDefinition = ErrorCodeDefinition::new( + }), + ) +}); + +static ROOT_QUERY_USED: LazyLock = + LazyLock::new(|| ROOT_TYPE_USED.create_code(SchemaRootKind::Query)); +static ROOT_MUTATION_USED: LazyLock = + LazyLock::new(|| ROOT_TYPE_USED.create_code(SchemaRootKind::Mutation)); +static ROOT_SUBSCRIPTION_USED: LazyLock = + LazyLock::new(|| ROOT_TYPE_USED.create_code(SchemaRootKind::Subscription)); + +static INVALID_SUBGRAPH_NAME: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "INVALID_SUBGRAPH_NAME".to_owned(), - "A subgraph name is invalid (subgraph names cannot be a single underscore (\"_\")).".to_owned(), + "A subgraph name is invalid (subgraph names cannot be a single underscore (\"_\"))." + .to_owned(), None, - ); + ) +}); - static ref NO_QUERIES: ErrorCodeDefinition = ErrorCodeDefinition::new( +static NO_QUERIES: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "NO_QUERIES".to_owned(), "None of the composed subgraphs expose any query.".to_owned(), None, - ); + ) +}); - static ref INTERFACE_FIELD_NO_IMPLEM: ErrorCodeDefinition = ErrorCodeDefinition::new( +static INTERFACE_FIELD_NO_IMPLEM: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "INTERFACE_FIELD_NO_IMPLEM".to_owned(), "After subgraph merging, an implementation is missing a field of one of the interface it implements (which can happen for valid subgraphs).".to_owned(), None, - ); + ) +}); - static ref TYPE_KIND_MISMATCH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static TYPE_KIND_MISMATCH: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "TYPE_KIND_MISMATCH".to_owned(), "A type has the same name in different subgraphs, but a different kind. For instance, one definition is an object type but another is an interface.".to_owned(), Some(ErrorCodeMetadata { replaces: &["VALUE_TYPE_KIND_MISMATCH", "EXTENSION_OF_WRONG_KIND", "ENUM_MISMATCH_TYPE"], ..DEFAULT_METADATA.clone() }), - ); + ) +}); - static ref EXTERNAL_TYPE_MISMATCH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static EXTERNAL_TYPE_MISMATCH: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "EXTERNAL_TYPE_MISMATCH".to_owned(), "An `@external` field has a type that is incompatible with the declaration(s) of that field in other subgraphs.".to_owned(), Some(ErrorCodeMetadata { added_in: FED1_CODE, replaces: &[], }), - ); + ) +}); - static ref EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE: ErrorCodeDefinition = ErrorCodeDefinition::new( - "EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE".to_owned(), - "The @external directive collides with other directives in some situations.".to_owned(), - Some(ErrorCodeMetadata { - added_in: "2.1.0", - replaces: &[], - }), - ); +static EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE: LazyLock = + LazyLock::new(|| { + ErrorCodeDefinition::new( + "EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE".to_owned(), + "The @external directive collides with other directives in some situations.".to_owned(), + Some(ErrorCodeMetadata { + added_in: "2.1.0", + replaces: &[], + }), + ) + }); - static ref EXTERNAL_ARGUMENT_MISSING: ErrorCodeDefinition = ErrorCodeDefinition::new( +static EXTERNAL_ARGUMENT_MISSING: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "EXTERNAL_ARGUMENT_MISSING".to_owned(), "An `@external` field is missing some arguments present in the declaration(s) of that field in other subgraphs.".to_owned(), None, - ); + ) +}); - static ref EXTERNAL_ARGUMENT_TYPE_MISMATCH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static EXTERNAL_ARGUMENT_TYPE_MISMATCH: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "EXTERNAL_ARGUMENT_TYPE_MISMATCH".to_owned(), "An `@external` field declares an argument with a type that is incompatible with the corresponding argument in the declaration(s) of that field in other subgraphs.".to_owned(), None, - ); - + ) +}); - static ref EXTERNAL_ARGUMENT_DEFAULT_MISMATCH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static EXTERNAL_ARGUMENT_DEFAULT_MISMATCH: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "EXTERNAL_ARGUMENT_DEFAULT_MISMATCH".to_owned(), "An `@external` field declares an argument with a default that is incompatible with the corresponding argument in the declaration(s) of that field in other subgraphs.".to_owned(), None, - ); + ) +}); - static ref EXTERNAL_ON_INTERFACE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static EXTERNAL_ON_INTERFACE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "EXTERNAL_ON_INTERFACE".to_owned(), "The field of an interface type is marked with `@external`: as external is about marking field not resolved by the subgraph and as interface field are not resolved (only implementations of those fields are), an \"external\" interface field is nonsensical".to_owned(), None, - ); + ) +}); - static ref MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL: ErrorCodeDefinition = ErrorCodeDefinition::new( +static MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL: LazyLock = LazyLock::new( + || { + ErrorCodeDefinition::new( "MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL".to_owned(), "In a subgraph, a field is both marked @external and has a merged directive applied to it".to_owned(), None, - ); + ) + }, +); - static ref FIELD_TYPE_MISMATCH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static FIELD_TYPE_MISMATCH: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "FIELD_TYPE_MISMATCH".to_owned(), "A field has a type that is incompatible with other declarations of that field in other subgraphs.".to_owned(), Some(ErrorCodeMetadata { replaces: &["VALUE_TYPE_FIELD_TYPE_MISMATCH"], ..DEFAULT_METADATA.clone() }), - ); + ) +}); - static ref FIELD_ARGUMENT_TYPE_MISMATCH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static FIELD_ARGUMENT_TYPE_MISMATCH: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "FIELD_ARGUMENT_TYPE_MISMATCH".to_owned(), "An argument (of a field/directive) has a type that is incompatible with that of other declarations of that same argument in other subgraphs.".to_owned(), Some(ErrorCodeMetadata { replaces: &["VALUE_TYPE_INPUT_VALUE_MISMATCH"], ..DEFAULT_METADATA.clone() }), - ); + ) +}); - static ref INPUT_FIELD_DEFAULT_MISMATCH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static INPUT_FIELD_DEFAULT_MISMATCH: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "INPUT_FIELD_DEFAULT_MISMATCH".to_owned(), "An input field has a default value that is incompatible with other declarations of that field in other subgraphs.".to_owned(), None, - ); + ) +}); - static ref FIELD_ARGUMENT_DEFAULT_MISMATCH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static FIELD_ARGUMENT_DEFAULT_MISMATCH: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "FIELD_ARGUMENT_DEFAULT_MISMATCH".to_owned(), "An argument (of a field/directive) has a default value that is incompatible with that of other declarations of that same argument in other subgraphs.".to_owned(), None, - ); + ) +}); - static ref EXTENSION_WITH_NO_BASE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static EXTENSION_WITH_NO_BASE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "EXTENSION_WITH_NO_BASE".to_owned(), "A subgraph is attempting to `extend` a type that is not originally defined in any known subgraph.".to_owned(), Some(ErrorCodeMetadata { added_in: FED1_CODE, replaces: &[], }), - ); + ) +}); - static ref EXTERNAL_MISSING_ON_BASE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static EXTERNAL_MISSING_ON_BASE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "EXTERNAL_MISSING_ON_BASE".to_owned(), "A field is marked as `@external` in a subgraph but with no non-external declaration in any other subgraph.".to_owned(), Some(ErrorCodeMetadata { added_in: FED1_CODE, replaces: &[], }), - ); + ) +}); - static ref INVALID_FIELD_SHARING: ErrorCodeDefinition = ErrorCodeDefinition::new( +static INVALID_FIELD_SHARING: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "INVALID_FIELD_SHARING".to_owned(), - "A field that is non-shareable in at least one subgraph is resolved by multiple subgraphs.".to_owned(), + "A field that is non-shareable in at least one subgraph is resolved by multiple subgraphs." + .to_owned(), None, - ); + ) +}); - static ref INVALID_SHAREABLE_USAGE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static INVALID_SHAREABLE_USAGE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "INVALID_SHAREABLE_USAGE".to_owned(), "The `@shareable` federation directive is used in an invalid way.".to_owned(), Some(ErrorCodeMetadata { added_in: "2.1.2", replaces: &[], }), - ); + ) +}); - static ref INVALID_LINK_DIRECTIVE_USAGE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static INVALID_LINK_DIRECTIVE_USAGE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "INVALID_LINK_DIRECTIVE_USAGE".to_owned(), - "An application of the @link directive is invalid/does not respect the specification.".to_owned(), + "An application of the @link directive is invalid/does not respect the specification." + .to_owned(), None, - ); + ) +}); - static ref INVALID_LINK_IDENTIFIER: ErrorCodeDefinition = ErrorCodeDefinition::new( +static INVALID_LINK_IDENTIFIER: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "INVALID_LINK_IDENTIFIER".to_owned(), - "A url/version for a @link feature is invalid/does not respect the specification.".to_owned(), + "A url/version for a @link feature is invalid/does not respect the specification." + .to_owned(), Some(ErrorCodeMetadata { added_in: "2.1.0", replaces: &[], }), - ); + ) +}); - static ref LINK_IMPORT_NAME_MISMATCH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static LINK_IMPORT_NAME_MISMATCH: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "LINK_IMPORT_NAME_MISMATCH".to_owned(), "The import name for a merged directive (as declared by the relevant `@link(import:)` argument) is inconsistent between subgraphs.".to_owned(), None, - ); + ) +}); - static ref REFERENCED_INACCESSIBLE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static REFERENCED_INACCESSIBLE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "REFERENCED_INACCESSIBLE".to_owned(), "An element is marked as @inaccessible but is referenced by an element visible in the API schema.".to_owned(), None, - ); + ) +}); - static ref DEFAULT_VALUE_USES_INACCESSIBLE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static DEFAULT_VALUE_USES_INACCESSIBLE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "DEFAULT_VALUE_USES_INACCESSIBLE".to_owned(), "An element is marked as @inaccessible but is used in the default value of an element visible in the API schema.".to_owned(), None, - ); + ) +}); - static ref QUERY_ROOT_TYPE_INACCESSIBLE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static QUERY_ROOT_TYPE_INACCESSIBLE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "QUERY_ROOT_TYPE_INACCESSIBLE".to_owned(), "An element is marked as @inaccessible but is the query root type, which must be visible in the API schema.".to_owned(), None, - ); + ) +}); - static ref REQUIRED_INACCESSIBLE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static REQUIRED_INACCESSIBLE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "REQUIRED_INACCESSIBLE".to_owned(), "An element is marked as @inaccessible but is required by an element visible in the API schema.".to_owned(), None, - ); + ) +}); - static ref IMPLEMENTED_BY_INACCESSIBLE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static IMPLEMENTED_BY_INACCESSIBLE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "IMPLEMENTED_BY_INACCESSIBLE".to_owned(), "An element is marked as @inaccessible but implements an element visible in the API schema.".to_owned(), None, - ); -} + ) +}); -// The above lazy_static! block hits recursion limit if we try to add more to it, so we start a -// new block here. -lazy_static! { - static ref DISALLOWED_INACCESSIBLE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static DISALLOWED_INACCESSIBLE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "DISALLOWED_INACCESSIBLE".to_owned(), "An element is marked as @inaccessible that is not allowed to be @inaccessible.".to_owned(), None, - ); + ) +}); - static ref ONLY_INACCESSIBLE_CHILDREN: ErrorCodeDefinition = ErrorCodeDefinition::new( +static ONLY_INACCESSIBLE_CHILDREN: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "ONLY_INACCESSIBLE_CHILDREN".to_owned(), "A type visible in the API schema has only @inaccessible children.".to_owned(), None, - ); + ) +}); - static ref REQUIRED_INPUT_FIELD_MISSING_IN_SOME_SUBGRAPH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static REQUIRED_INPUT_FIELD_MISSING_IN_SOME_SUBGRAPH: LazyLock = LazyLock::new( + || { + ErrorCodeDefinition::new( "REQUIRED_INPUT_FIELD_MISSING_IN_SOME_SUBGRAPH".to_owned(), "A field of an input object type is mandatory in some subgraphs, but the field is not defined in all the subgraphs that define the input object type.".to_owned(), None, - ); + ) + }, +); - static ref REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH: LazyLock = LazyLock::new( + || { + ErrorCodeDefinition::new( "REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH".to_owned(), "An argument of a field or directive definition is mandatory in some subgraphs, but the argument is not defined in all the subgraphs that define the field or directive definition.".to_owned(), None, - ); + ) + }, +); - static ref EMPTY_MERGED_INPUT_TYPE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static EMPTY_MERGED_INPUT_TYPE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "EMPTY_MERGED_INPUT_TYPE".to_owned(), "An input object type has no field common to all the subgraphs that define the type. Merging that type would result in an invalid empty input object type.".to_owned(), None, - ); + ) +}); - static ref ENUM_VALUE_MISMATCH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static ENUM_VALUE_MISMATCH: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "ENUM_VALUE_MISMATCH".to_owned(), "An enum type that is used as both an input and output type has a value that is not defined in all the subgraphs that define the enum type.".to_owned(), None, - ); + ) +}); - static ref EMPTY_MERGED_ENUM_TYPE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static EMPTY_MERGED_ENUM_TYPE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "EMPTY_MERGED_ENUM_TYPE".to_owned(), "An enum type has no value common to all the subgraphs that define the type. Merging that type would result in an invalid empty enum type.".to_owned(), None, - ); + ) +}); - static ref SHAREABLE_HAS_MISMATCHED_RUNTIME_TYPES: ErrorCodeDefinition = ErrorCodeDefinition::new( +static SHAREABLE_HAS_MISMATCHED_RUNTIME_TYPES: LazyLock = LazyLock::new( + || { + ErrorCodeDefinition::new( "SHAREABLE_HAS_MISMATCHED_RUNTIME_TYPES".to_owned(), "A shareable field return type has mismatched possible runtime types in the subgraphs in which the field is declared. As shared fields must resolve the same way in all subgraphs, this is almost surely a mistake.".to_owned(), None, - ); + ) + }, +); - static ref SATISFIABILITY_ERROR: ErrorCodeDefinition = ErrorCodeDefinition::new( +static SATISFIABILITY_ERROR: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "SATISFIABILITY_ERROR".to_owned(), "Subgraphs can be merged, but the resulting supergraph API would have queries that cannot be satisfied by those subgraphs.".to_owned(), None, - ); + ) +}); - static ref OVERRIDE_FROM_SELF_ERROR: ErrorCodeDefinition = ErrorCodeDefinition::new( +static OVERRIDE_FROM_SELF_ERROR: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "OVERRIDE_FROM_SELF_ERROR".to_owned(), - "Field with `@override` directive has \"from\" location that references its own subgraph.".to_owned(), + "Field with `@override` directive has \"from\" location that references its own subgraph." + .to_owned(), None, - ); + ) +}); - static ref OVERRIDE_SOURCE_HAS_OVERRIDE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static OVERRIDE_SOURCE_HAS_OVERRIDE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "OVERRIDE_SOURCE_HAS_OVERRIDE".to_owned(), "Field which is overridden to another subgraph is also marked @override.".to_owned(), None, - ); + ) +}); - static ref OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE: LazyLock = LazyLock::new( + || { + ErrorCodeDefinition::new( "OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE".to_owned(), "The @override directive cannot be used on external fields, nor to override fields with either @external, @provides, or @requires.".to_owned(), None, - ); + ) + }, +); - static ref OVERRIDE_ON_INTERFACE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static OVERRIDE_ON_INTERFACE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "OVERRIDE_ON_INTERFACE".to_owned(), "The @override directive cannot be used on the fields of an interface type.".to_owned(), Some(ErrorCodeMetadata { added_in: "2.3.0", replaces: &[], }), - ); + ) +}); - static ref UNSUPPORTED_FEATURE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static UNSUPPORTED_FEATURE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "UNSUPPORTED_FEATURE".to_owned(), "Indicates an error due to feature currently unsupported by federation.".to_owned(), Some(ErrorCodeMetadata { added_in: "2.1.0", replaces: &[], }), - ); + ) +}); - static ref INVALID_FEDERATION_SUPERGRAPH: ErrorCodeDefinition = ErrorCodeDefinition::new( +static INVALID_FEDERATION_SUPERGRAPH: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "INVALID_FEDERATION_SUPERGRAPH".to_owned(), "Indicates that a schema provided for an Apollo Federation supergraph is not a valid supergraph schema.".to_owned(), Some(ErrorCodeMetadata { added_in: "2.1.0", replaces: &[], }), - ); + ) +}); - static ref DOWNSTREAM_SERVICE_ERROR: ErrorCodeDefinition = ErrorCodeDefinition::new( +static DOWNSTREAM_SERVICE_ERROR: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "DOWNSTREAM_SERVICE_ERROR".to_owned(), "Indicates an error in a subgraph service query during query execution in a federated service.".to_owned(), Some(ErrorCodeMetadata { added_in: FED1_CODE, replaces: &[], }), - ); + ) +}); - static ref DIRECTIVE_COMPOSITION_ERROR: ErrorCodeDefinition = ErrorCodeDefinition::new( +static DIRECTIVE_COMPOSITION_ERROR: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "DIRECTIVE_COMPOSITION_ERROR".to_owned(), "Error when composing custom directives.".to_owned(), Some(ErrorCodeMetadata { added_in: "2.1.0", replaces: &[], }), - ); + ) +}); - static ref INTERFACE_OBJECT_USAGE_ERROR: ErrorCodeDefinition = ErrorCodeDefinition::new( +static INTERFACE_OBJECT_USAGE_ERROR: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "INTERFACE_OBJECT_USAGE_ERROR".to_owned(), "Error in the usage of the @interfaceObject directive.".to_owned(), Some(ErrorCodeMetadata { added_in: "2.3.0", replaces: &[], }), - ); + ) +}); - static ref INTERFACE_KEY_NOT_ON_IMPLEMENTATION: ErrorCodeDefinition = ErrorCodeDefinition::new( +static INTERFACE_KEY_NOT_ON_IMPLEMENTATION: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "INTERFACE_KEY_NOT_ON_IMPLEMENTATION".to_owned(), "A `@key` is defined on an interface type, but is not defined (or is not resolvable) on at least one of the interface implementations".to_owned(), Some(ErrorCodeMetadata { added_in: "2.3.0", replaces: &[], }), - ); + ) +}); - static ref INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE: LazyLock = LazyLock::new( + || { + ErrorCodeDefinition::new( "INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE".to_owned(), "A subgraph has a `@key` on an interface type, but that subgraph does not define an implementation (in the supergraph) of that interface".to_owned(), Some(ErrorCodeMetadata { added_in: "2.3.0", replaces: &[], }), - ); + ) + }, +); - static ref INTERNAL: ErrorCodeDefinition = ErrorCodeDefinition::new( +static INTERNAL: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "INTERNAL".to_owned(), "An internal federation error occured.".to_owned(), None, - ); + ) +}); - static ref UNSUPPORTED_FEDERATION_VERSION: ErrorCodeDefinition = ErrorCodeDefinition::new( +static UNSUPPORTED_FEDERATION_VERSION: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "UNSUPPORTED_FEDERATION_VERSION".to_owned(), "Supergraphs composed with federation version 1 are not supported. Please recompose your supergraph with federation version 2 or greater".to_owned(), None, - ); + ) +}); - static ref UNSUPPORTED_FEDERATION_DIRECTIVE: ErrorCodeDefinition = ErrorCodeDefinition::new( +static UNSUPPORTED_FEDERATION_DIRECTIVE: LazyLock = LazyLock::new(|| { + ErrorCodeDefinition::new( "UNSUPPORTED_FEDERATION_DIRECTIVE".to_owned(), - "Indicates that the specified specification version is outside of supported range".to_owned(), + "Indicates that the specified specification version is outside of supported range" + .to_owned(), None, - - ); -} + ) +}); #[derive(Debug, strum_macros::EnumIter)] pub enum ErrorCode { diff --git a/apollo-federation/src/link/context_spec_definition.rs b/apollo-federation/src/link/context_spec_definition.rs index 70ae8af6ad..80908b4990 100644 --- a/apollo-federation/src/link/context_spec_definition.rs +++ b/apollo-federation/src/link/context_spec_definition.rs @@ -1,9 +1,10 @@ +use std::sync::LazyLock; + use apollo_compiler::ast::Directive; use apollo_compiler::ast::DirectiveDefinition; use apollo_compiler::name; use apollo_compiler::Name; use apollo_compiler::Node; -use lazy_static::lazy_static; use crate::error::FederationError; use crate::internal_error; @@ -61,10 +62,9 @@ impl SpecDefinition for ContextSpecDefinition { } } -lazy_static! { - pub(crate) static ref CONTEXT_VERSIONS: SpecDefinitions = { +pub(crate) static CONTEXT_VERSIONS: LazyLock> = + LazyLock::new(|| { let mut definitions = SpecDefinitions::new(Identity::context_identity()); definitions.add(ContextSpecDefinition::new(Version { major: 0, minor: 1 })); definitions - }; -} + }); diff --git a/apollo-federation/src/link/cost_spec_definition.rs b/apollo-federation/src/link/cost_spec_definition.rs index 66a5e91aa9..e8bb720465 100644 --- a/apollo-federation/src/link/cost_spec_definition.rs +++ b/apollo-federation/src/link/cost_spec_definition.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::sync::LazyLock; use apollo_compiler::ast::Argument; use apollo_compiler::ast::Directive; @@ -10,7 +11,6 @@ use apollo_compiler::schema::Component; use apollo_compiler::schema::ExtendedType; use apollo_compiler::Name; use apollo_compiler::Node; -use lazy_static::lazy_static; use crate::error::FederationError; use crate::internal_error; @@ -242,13 +242,12 @@ impl SpecDefinition for CostSpecDefinition { } } -lazy_static! { - pub(crate) static ref COST_VERSIONS: SpecDefinitions = { +pub(crate) static COST_VERSIONS: LazyLock> = + LazyLock::new(|| { let mut definitions = SpecDefinitions::new(Identity::cost_identity()); definitions.add(CostSpecDefinition::new(Version { major: 0, minor: 1 })); definitions - }; -} + }); pub struct CostDirective { weight: i32, diff --git a/apollo-federation/src/link/federation_spec_definition.rs b/apollo-federation/src/link/federation_spec_definition.rs index 7f5e041aaf..d161fb1269 100644 --- a/apollo-federation/src/link/federation_spec_definition.rs +++ b/apollo-federation/src/link/federation_spec_definition.rs @@ -1,3 +1,5 @@ +use std::sync::LazyLock; + use apollo_compiler::ast::Argument; use apollo_compiler::name; use apollo_compiler::schema::Directive; @@ -7,7 +9,6 @@ use apollo_compiler::schema::UnionType; use apollo_compiler::schema::Value; use apollo_compiler::Name; use apollo_compiler::Node; -use lazy_static::lazy_static; use crate::error::FederationError; use crate::error::SingleFederationError; @@ -545,8 +546,8 @@ impl SpecDefinition for FederationSpecDefinition { } } -lazy_static! { - pub(crate) static ref FEDERATION_VERSIONS: SpecDefinitions = { +pub(crate) static FEDERATION_VERSIONS: LazyLock> = + LazyLock::new(|| { let mut definitions = SpecDefinitions::new(Identity::federation_identity()); definitions.add(FederationSpecDefinition::new(Version { major: 2, @@ -589,8 +590,7 @@ lazy_static! { minor: 9, })); definitions - }; -} + }); pub(crate) fn get_federation_spec_definition_from_subgraph( schema: &FederationSchema, diff --git a/apollo-federation/src/link/inaccessible_spec_definition.rs b/apollo-federation/src/link/inaccessible_spec_definition.rs index 295fb3d7b3..801de88dc9 100644 --- a/apollo-federation/src/link/inaccessible_spec_definition.rs +++ b/apollo-federation/src/link/inaccessible_spec_definition.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::sync::LazyLock; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; @@ -13,7 +14,6 @@ use apollo_compiler::schema::InputValueDefinition; use apollo_compiler::schema::Value; use apollo_compiler::Name; use apollo_compiler::Node; -use lazy_static::lazy_static; use crate::error::FederationError; use crate::error::MultipleFederationErrors; @@ -97,8 +97,8 @@ impl SpecDefinition for InaccessibleSpecDefinition { } } -lazy_static! { - pub(crate) static ref INACCESSIBLE_VERSIONS: SpecDefinitions = { +pub(crate) static INACCESSIBLE_VERSIONS: LazyLock> = + LazyLock::new(|| { let mut definitions = SpecDefinitions::new(Identity::inaccessible_identity()); definitions.add(InaccessibleSpecDefinition::new(Version { major: 0, @@ -109,8 +109,7 @@ lazy_static! { minor: 2, })); definitions - }; -} + }); fn is_type_system_location(location: DirectiveLocation) -> bool { matches!( diff --git a/apollo-federation/src/link/join_spec_definition.rs b/apollo-federation/src/link/join_spec_definition.rs index 31f98eedf1..e8c3dca42c 100644 --- a/apollo-federation/src/link/join_spec_definition.rs +++ b/apollo-federation/src/link/join_spec_definition.rs @@ -1,3 +1,5 @@ +use std::sync::LazyLock; + use apollo_compiler::ast::Value; use apollo_compiler::name; use apollo_compiler::schema::Directive; @@ -7,7 +9,6 @@ use apollo_compiler::schema::ExtendedType; use apollo_compiler::Name; use apollo_compiler::Node; use itertools::Itertools; -use lazy_static::lazy_static; use super::argument::directive_optional_list_argument; use crate::bail; @@ -410,8 +411,8 @@ impl SpecDefinition for JoinSpecDefinition { } } -lazy_static! { - pub(crate) static ref JOIN_VERSIONS: SpecDefinitions = { +pub(crate) static JOIN_VERSIONS: LazyLock> = + LazyLock::new(|| { let mut definitions = SpecDefinitions::new(Identity::join_identity()); definitions.add(JoinSpecDefinition::new(Version { major: 0, minor: 1 })); definitions.add(JoinSpecDefinition::new(Version { major: 0, minor: 2 })); @@ -419,5 +420,4 @@ lazy_static! { definitions.add(JoinSpecDefinition::new(Version { major: 0, minor: 4 })); definitions.add(JoinSpecDefinition::new(Version { major: 0, minor: 5 })); definitions - }; -} + }); diff --git a/apollo-federation/src/link/link_spec_definition.rs b/apollo-federation/src/link/link_spec_definition.rs index 59e75e42e1..2e84cf321b 100644 --- a/apollo-federation/src/link/link_spec_definition.rs +++ b/apollo-federation/src/link/link_spec_definition.rs @@ -1,4 +1,4 @@ -use lazy_static::lazy_static; +use std::sync::LazyLock; use crate::link::spec::Identity; use crate::link::spec::Url; @@ -24,8 +24,8 @@ impl SpecDefinition for LinkSpecDefinition { } } -lazy_static! { - pub(crate) static ref CORE_VERSIONS: SpecDefinitions = { +pub(crate) static CORE_VERSIONS: LazyLock> = + LazyLock::new(|| { let mut definitions = SpecDefinitions::new(Identity::core_identity()); definitions.add(LinkSpecDefinition::new( Version { major: 0, minor: 1 }, @@ -36,13 +36,13 @@ lazy_static! { Identity::core_identity(), )); definitions - }; - pub(crate) static ref LINK_VERSIONS: SpecDefinitions = { + }); +pub(crate) static LINK_VERSIONS: LazyLock> = + LazyLock::new(|| { let mut definitions = SpecDefinitions::new(Identity::link_identity()); definitions.add(LinkSpecDefinition::new( Version { major: 1, minor: 0 }, Identity::link_identity(), )); definitions - }; -} + }); diff --git a/apollo-federation/src/query_graph/build_query_graph.rs b/apollo-federation/src/query_graph/build_query_graph.rs index 0c820c5622..77ff9ec802 100644 --- a/apollo-federation/src/query_graph/build_query_graph.rs +++ b/apollo-federation/src/query_graph/build_query_graph.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::sync::LazyLock; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; @@ -8,7 +9,6 @@ use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_compiler::Schema; use itertools::Itertools; -use lazy_static::lazy_static; use petgraph::graph::EdgeIndex; use petgraph::graph::NodeIndex; use petgraph::visit::EdgeRef; @@ -2357,12 +2357,11 @@ fn resolvable_key_applications<'doc>( Ok(applications) } -lazy_static! { - static ref CONTEXT_PARSING_LEADING_PATTERN: Regex = - Regex::new(r#"^(?:[\n\r\t ,]|#[^\n\r]*)*((?s:.)*)$"#).unwrap(); - static ref CONTEXT_PARSING_CONTEXT_PATTERN: Regex = - Regex::new(r#"^([A-Za-z_](?-u:\w)*)((?s:.)*)$"#).unwrap(); -} +static CONTEXT_PARSING_LEADING_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r#"^(?:[\n\r\t ,]|#[^\n\r]*)*((?s:.)*)$"#).unwrap()); + +static CONTEXT_PARSING_CONTEXT_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r#"^([A-Za-z_](?-u:\w)*)((?s:.)*)$"#).unwrap()); fn parse_context(field: &str) -> Result<(String, String), FederationError> { // PORT_NOTE: The original JS regex, as shown below diff --git a/apollo-federation/src/schema/argument_composition_strategies.rs b/apollo-federation/src/schema/argument_composition_strategies.rs index e231d98222..70586da3c5 100644 --- a/apollo-federation/src/schema/argument_composition_strategies.rs +++ b/apollo-federation/src/schema/argument_composition_strategies.rs @@ -1,7 +1,8 @@ +use std::sync::LazyLock; + use apollo_compiler::ast::Value; use apollo_compiler::name; use apollo_compiler::schema::Type; -use lazy_static::lazy_static; use crate::schema::FederationSchema; @@ -15,18 +16,16 @@ pub(crate) enum ArgumentCompositionStrategy { Union, } -lazy_static! { - pub(crate) static ref MAX_STRATEGY: MaxArgumentCompositionStrategy = - MaxArgumentCompositionStrategy::new(); - pub(crate) static ref MIN_STRATEGY: MinArgumentCompositionStrategy = - MinArgumentCompositionStrategy::new(); - pub(crate) static ref SUM_STRATEGY: SumArgumentCompositionStrategy = - SumArgumentCompositionStrategy::new(); - pub(crate) static ref INTERSECTION_STRATEGY: IntersectionArgumentCompositionStrategy = - IntersectionArgumentCompositionStrategy {}; - pub(crate) static ref UNION_STRATEGY: UnionArgumentCompositionStrategy = - UnionArgumentCompositionStrategy {}; -} +pub(crate) static MAX_STRATEGY: LazyLock = + LazyLock::new(MaxArgumentCompositionStrategy::new); +pub(crate) static MIN_STRATEGY: LazyLock = + LazyLock::new(MinArgumentCompositionStrategy::new); +pub(crate) static SUM_STRATEGY: LazyLock = + LazyLock::new(SumArgumentCompositionStrategy::new); +pub(crate) static INTERSECTION_STRATEGY: LazyLock = + LazyLock::new(|| IntersectionArgumentCompositionStrategy {}); +pub(crate) static UNION_STRATEGY: LazyLock = + LazyLock::new(|| UnionArgumentCompositionStrategy {}); impl ArgumentCompositionStrategy { pub(crate) fn name(&self) -> &str { diff --git a/apollo-federation/src/subgraph/spec.rs b/apollo-federation/src/subgraph/spec.rs index 24a96ba2b4..22daa3a781 100644 --- a/apollo-federation/src/subgraph/spec.rs +++ b/apollo-federation/src/subgraph/spec.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::sync::LazyLock; use apollo_compiler::ast::Argument; use apollo_compiler::ast::Directive; @@ -23,7 +24,6 @@ use apollo_compiler::ty; use apollo_compiler::InvalidNameError; use apollo_compiler::Name; use apollo_compiler::Node; -use lazy_static::lazy_static; use thiserror::Error; use crate::link::spec::Identity; @@ -105,8 +105,8 @@ enum FederationDirectiveName { Tag, } -lazy_static! { - static ref FEDERATION_DIRECTIVE_NAMES_TO_ENUM: IndexMap = { +static FEDERATION_DIRECTIVE_NAMES_TO_ENUM: LazyLock> = + LazyLock::new(|| { IndexMap::from_iter([ (COMPOSE_DIRECTIVE_NAME, FederationDirectiveName::Compose), (CONTEXT_DIRECTIVE_NAME, FederationDirectiveName::Context), @@ -131,8 +131,7 @@ lazy_static! { (SHAREABLE_DIRECTIVE_NAME, FederationDirectiveName::Shareable), (TAG_DIRECTIVE_NAME, FederationDirectiveName::Tag), ]) - }; -} + }); const MIN_FEDERATION_VERSION: Version = Version { major: 2, minor: 0 }; const MAX_FEDERATION_VERSION: Version = Version { major: 2, minor: 5 }; diff --git a/apollo-federation/src/supergraph/mod.rs b/apollo-federation/src/supergraph/mod.rs index d4912ad4d7..d44978db9d 100644 --- a/apollo-federation/src/supergraph/mod.rs +++ b/apollo-federation/src/supergraph/mod.rs @@ -5,6 +5,7 @@ use std::fmt::Write; use std::ops::Deref; use std::ops::Not; use std::sync::Arc; +use std::sync::LazyLock; use apollo_compiler::ast::Argument; use apollo_compiler::ast::Directive; @@ -35,7 +36,6 @@ use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_compiler::Node; use itertools::Itertools; -use lazy_static::lazy_static; use time::OffsetDateTime; pub(crate) use self::schema::new_empty_fed_2_subgraph_schema; @@ -1553,8 +1553,8 @@ fn get_subgraph<'subgraph>( }) } -lazy_static! { - static ref EXECUTABLE_DIRECTIVE_LOCATIONS: IndexSet = { +static EXECUTABLE_DIRECTIVE_LOCATIONS: LazyLock> = + LazyLock::new(|| { [ DirectiveLocation::Query, DirectiveLocation::Mutation, @@ -1567,8 +1567,7 @@ lazy_static! { ] .into_iter() .collect() - }; -} + }); fn remove_unused_types_from_subgraph(schema: &mut FederationSchema) -> Result<(), FederationError> { // We now do an additional path on all types because we sometimes added types to subgraphs diff --git a/apollo-router-scaffold/templates/base/rust-toolchain.toml b/apollo-router-scaffold/templates/base/rust-toolchain.toml index 08f13cdb9f..8de91b2c20 100644 --- a/apollo-router-scaffold/templates/base/rust-toolchain.toml +++ b/apollo-router-scaffold/templates/base/rust-toolchain.toml @@ -2,5 +2,5 @@ [toolchain] # renovate-automation: rustc version -channel = "1.76.0" +channel = "1.83.0" components = ["rustfmt", "clippy"] diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 3615b79d0a..dc1551d99d 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -8,7 +8,7 @@ description = "A configurable, high-performance routing runtime for Apollo Feder license = "Elastic-2.0" # renovate-automation: rustc version -rust-version = "1.76.0" +rust-version = "1.83.0" edition = "2021" build = "build/main.rs" @@ -110,7 +110,6 @@ jsonpath_lib = "0.3.0" jsonpath-rust = "0.3.5" jsonschema = { version = "0.17.1", default-features = false } jsonwebtoken = "9.3.0" -lazy_static = "1.4.0" libc = "0.2.155" linkme = "0.3.27" lru = "0.12.3" diff --git a/apollo-router/src/error.rs b/apollo-router/src/error.rs index 04efe566e9..d20f35cc39 100644 --- a/apollo-router/src/error.rs +++ b/apollo-router/src/error.rs @@ -1,12 +1,12 @@ //! Router errors. use std::collections::HashMap; +use std::ops::Deref; use std::sync::Arc; use apollo_compiler::validation::DiagnosticList; use apollo_compiler::validation::WithErrors; use apollo_federation::error::FederationError; use displaydoc::Display; -use lazy_static::__Deref; use serde::Deserialize; use serde::Serialize; use thiserror::Error; diff --git a/apollo-router/src/plugins/telemetry/otlp.rs b/apollo-router/src/plugins/telemetry/otlp.rs index e1f4dfe966..50985c28e9 100644 --- a/apollo-router/src/plugins/telemetry/otlp.rs +++ b/apollo-router/src/plugins/telemetry/otlp.rs @@ -1,11 +1,11 @@ //! Shared configuration for Otlp tracing and metrics. use std::collections::HashMap; use std::str::FromStr; +use std::sync::LazyLock; use http::uri::Parts; use http::uri::PathAndQuery; use http::Uri; -use lazy_static::lazy_static; use opentelemetry::sdk::metrics::reader::TemporalitySelector; use opentelemetry::sdk::metrics::InstrumentKind; use opentelemetry_otlp::HttpExporterBuilder; @@ -26,10 +26,10 @@ use crate::plugins::telemetry::config::GenericWith; use crate::plugins::telemetry::endpoint::UriEndpoint; use crate::plugins::telemetry::tracing::BatchProcessorConfig; -lazy_static! { - static ref DEFAULT_GRPC_ENDPOINT: Uri = Uri::from_static("http://127.0.0.1:4317"); - static ref DEFAULT_HTTP_ENDPOINT: Uri = Uri::from_static("http://127.0.0.1:4318"); -} +static DEFAULT_GRPC_ENDPOINT: LazyLock = + LazyLock::new(|| Uri::from_static("http://127.0.0.1:4317")); +static DEFAULT_HTTP_ENDPOINT: LazyLock = + LazyLock::new(|| Uri::from_static("http://127.0.0.1:4318")); const DEFAULT_HTTP_ENDPOINT_PATH: &str = "/v1/traces"; diff --git a/apollo-router/src/plugins/telemetry/tracing/jaeger.rs b/apollo-router/src/plugins/telemetry/tracing/jaeger.rs index f50aebefc2..bce59978ae 100644 --- a/apollo-router/src/plugins/telemetry/tracing/jaeger.rs +++ b/apollo-router/src/plugins/telemetry/tracing/jaeger.rs @@ -1,8 +1,8 @@ //! Configuration for jaeger tracing. use std::fmt::Debug; +use std::sync::LazyLock; use http::Uri; -use lazy_static::lazy_static; use opentelemetry::runtime; use opentelemetry::sdk::trace::BatchSpanProcessor; use opentelemetry::sdk::trace::Builder; @@ -19,9 +19,9 @@ use crate::plugins::telemetry::tracing::BatchProcessorConfig; use crate::plugins::telemetry::tracing::SpanProcessorExt; use crate::plugins::telemetry::tracing::TracingConfigurator; -lazy_static! { - static ref DEFAULT_ENDPOINT: Uri = Uri::from_static("http://127.0.0.1:14268/api/traces"); -} +static DEFAULT_ENDPOINT: LazyLock = + LazyLock::new(|| Uri::from_static("http://127.0.0.1:14268/api/traces")); + #[derive(Debug, Clone, Deserialize, JsonSchema)] #[serde(deny_unknown_fields, untagged)] pub(crate) enum Config { diff --git a/apollo-router/src/plugins/telemetry/tracing/zipkin.rs b/apollo-router/src/plugins/telemetry/tracing/zipkin.rs index 1a9ccbd654..82160c3bef 100644 --- a/apollo-router/src/plugins/telemetry/tracing/zipkin.rs +++ b/apollo-router/src/plugins/telemetry/tracing/zipkin.rs @@ -1,6 +1,7 @@ //! Configuration for zipkin tracing. +use std::sync::LazyLock; + use http::Uri; -use lazy_static::lazy_static; use opentelemetry::sdk; use opentelemetry::sdk::trace::BatchSpanProcessor; use opentelemetry::sdk::trace::Builder; @@ -17,9 +18,8 @@ use crate::plugins::telemetry::tracing::BatchProcessorConfig; use crate::plugins::telemetry::tracing::SpanProcessorExt; use crate::plugins::telemetry::tracing::TracingConfigurator; -lazy_static! { - static ref DEFAULT_ENDPOINT: Uri = Uri::from_static("http://127.0.0.1:9411/api/v2/spans"); -} +static DEFAULT_ENDPOINT: LazyLock = + LazyLock::new(|| Uri::from_static("http://127.0.0.1:9411/api/v2/spans")); #[derive(Debug, Clone, Deserialize, JsonSchema, Default)] #[serde(deny_unknown_fields)] From bfeca6e231c8f987a6204da59fb0e89e31842284 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 13 Dec 2024 10:44:41 +0000 Subject: [PATCH 22/35] Improve integration tester --- apollo-router/tests/common.rs | 257 ++++--- apollo-router/tests/integration/batching.rs | 4 +- .../tests/integration/coprocessor.rs | 4 +- .../tests/integration/introspection.rs | 8 +- .../tests/integration/operation_limits.rs | 6 +- .../query_planner/max_evaluated_plans.rs | 14 +- .../tests/integration/subgraph_response.rs | 22 +- .../tests/integration/subscription.rs | 5 +- apollo-router/tests/integration/supergraph.rs | 13 +- .../tests/integration/telemetry/datadog.rs | 616 ++++++--------- ...adog_parent_sampler_very_small.router.yaml | 29 + ...request_with_zipkin_propagator.router.yaml | 1 + .../tests/integration/telemetry/jaeger.rs | 639 ++++++---------- .../tests/integration/telemetry/logging.rs | 79 +- .../tests/integration/telemetry/metrics.rs | 26 +- .../tests/integration/telemetry/mod.rs | 18 + .../tests/integration/telemetry/otlp.rs | 706 ++++++++---------- .../integration/telemetry/propagation.rs | 5 +- .../tests/integration/telemetry/verifier.rs | 160 ++++ .../tests/integration/telemetry/zipkin.rs | 211 +++--- .../tests/integration/traffic_shaping.rs | 6 +- apollo-router/tests/samples_tests.rs | 3 +- 22 files changed, 1309 insertions(+), 1523 deletions(-) create mode 100644 apollo-router/tests/integration/telemetry/fixtures/datadog_parent_sampler_very_small.router.yaml create mode 100644 apollo-router/tests/integration/telemetry/verifier.rs diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index 2fa1d8a024..5374059ccf 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; -use buildstructor::buildstructor; +use buildstructor::{buildstructor}; use fred::clients::RedisClient; use fred::interfaces::ClientLike; use fred::interfaces::KeysInterface; @@ -18,7 +18,6 @@ use fred::types::Scanner; use futures::StreamExt; use http::header::ACCEPT; use http::header::CONTENT_TYPE; -use http::HeaderValue; use mediatype::names::BOUNDARY; use mediatype::names::FORM_DATA; use mediatype::names::MULTIPART; @@ -70,6 +69,65 @@ use wiremock::Mock; use wiremock::Respond; use wiremock::ResponseTemplate; +pub struct Query { + traced: bool, + psr: Option<&'static str>, + headers: HashMap, + content_type: String, + body: Value, +} + +impl Default for Query { + fn default() -> Self { + Query::builder().build() + } +} + +#[buildstructor::buildstructor] +impl Query { + #[builder] + pub fn new( + traced: Option, + psr: Option<&'static str>, + body: Option, + content_type: Option, + headers: HashMap, + ) -> Self { + Self { + traced: traced.unwrap_or(true), + psr, + body: body.unwrap_or( + json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}), + ), + content_type: content_type + .unwrap_or_else(|| APPLICATION_JSON.essence_str().to_string()), + headers, + } + } +} +impl Query { + pub fn with_bad_content_type(mut self) -> Self { + self.content_type = "garbage".to_string(); + self + } + + pub fn with_bad_query(mut self) -> Self { + self.body = json!({"garbage":{}}); + self + } + + pub fn with_huge_query(mut self) -> Self { + self.body = json!({"query":"query {topProducts{name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name}}","variables":{}}); + self + } + + pub fn introspection() -> Query { + Query::builder() + .body(json!({"query":"{__schema {types {name}}}","variables":{}})) + .build() + } +} + pub struct IntegrationTest { router: Option, test_config_location: PathBuf, @@ -90,6 +148,7 @@ pub struct IntegrationTest { bind_address: Arc>>, redis_namespace: String, log: String, + subgraph_context: Arc>>, } impl IntegrationTest { @@ -104,6 +163,7 @@ impl IntegrationTest { struct TracedResponder { response_template: ResponseTemplate, telemetry: Telemetry, + extra_propagator: Telemetry, subscriber_subgraph: Dispatch, subgraph_callback: Option>, subgraph_context: Arc>>, @@ -111,10 +171,11 @@ struct TracedResponder { impl Respond for TracedResponder { fn respond(&self, request: &wiremock::Request) -> ResponseTemplate { - let context = self.telemetry.extract_context(request); + let context = self.telemetry.extract_context(request, &Context::new()); + let context = self.extra_propagator.extract_context(request, &context); + *self.subgraph_context.lock().expect("lock poisoned") = Some(context.span().span_context().clone()); - tracing_core::dispatcher::with_default(&self.subscriber_subgraph, || { let _context_guard = context.attach(); let span = info_span!("subgraph server"); @@ -264,7 +325,7 @@ impl Telemetry { } } - pub(crate) fn extract_context(&self, request: &wiremock::Request) -> Context { + pub(crate) fn extract_context(&self, request: &wiremock::Request, context: &Context) -> Context { let headers: HashMap = request .headers .iter() @@ -274,11 +335,13 @@ impl Telemetry { match self { Telemetry::Jaeger => { let propagator = opentelemetry_jaeger::Propagator::new(); - propagator.extract(&headers) + propagator.extract_with_context(context, &headers) } Telemetry::Datadog => { + let span_ref = context.span(); + let original_span_context = span_ref.span_context(); let propagator = opentelemetry_datadog::DatadogPropagator::new(); - let mut context = propagator.extract(&headers); + let mut context = propagator.extract_with_context(context, &headers); // We're going to override the sampled so that we can test sampling priority if let Some(psr) = headers.get("x-datadog-sampling-priority") { let state = context @@ -287,8 +350,14 @@ impl Telemetry { .trace_state() .insert("psr", psr.to_string()) .expect("psr"); + let new_trace_id = if original_span_context.is_valid() { + original_span_context.trace_id() + } + else { + context.span().span_context().trace_id() + }; context = context.with_remote_span_context(SpanContext::new( - context.span().span_context().trace_id(), + new_trace_id, context.span().span_context().span_id(), context.span().span_context().trace_flags(), true, @@ -300,13 +369,13 @@ impl Telemetry { } Telemetry::Otlp { .. } => { let propagator = opentelemetry::sdk::propagation::TraceContextPropagator::default(); - propagator.extract(&headers) + propagator.extract_with_context(context, &headers) } Telemetry::Zipkin => { let propagator = opentelemetry_zipkin::Propagator::new(); - propagator.extract(&headers) + propagator.extract_with_context(context, &headers) } - _ => Context::current(), + _ => context.clone(), } } } @@ -319,7 +388,6 @@ impl IntegrationTest { telemetry: Option, extra_propagator: Option, responder: Option, - subgraph_context: Option>>>, collect_stdio: Option>, supergraph: Option, mut subgraph_overrides: HashMap, @@ -354,13 +422,15 @@ impl IntegrationTest { .start() .await; + let subgraph_context = Arc::new(Mutex::new(None)); Mock::given(method("POST")) .respond_with(TracedResponder{response_template:responder.unwrap_or_else(|| ResponseTemplate::new(200).set_body_json(json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}))), telemetry: telemetry.clone(), + extra_propagator: extra_propagator.clone(), subscriber_subgraph: Self::dispatch(&tracer_provider_subgraph), subgraph_callback, - subgraph_context: subgraph_context.unwrap_or_default() + subgraph_context: subgraph_context.clone() }) .mount(&subgraphs) .await; @@ -398,6 +468,7 @@ impl IntegrationTest { extra_propagator, redis_namespace, log: log.unwrap_or_else(|| "error,apollo_router=info".to_owned()), + subgraph_context, } } @@ -415,6 +486,15 @@ impl IntegrationTest { Dispatch::new(subscriber) } + pub fn subgraph_context(&self) -> SpanContext { + self.subgraph_context + .lock() + .expect("lock poisoned") + .as_ref() + .unwrap() + .clone() + } + pub fn router_location() -> PathBuf { PathBuf::from(env!("CARGO_BIN_EXE_router")) } @@ -541,60 +621,15 @@ impl IntegrationTest { fs::copy(supergraph_path, &self.test_schema_location).expect("could not write schema"); } - #[allow(dead_code)] pub fn execute_default_query( &self, ) -> impl std::future::Future { - self.execute_query_internal( - &json!({"query":"query {topProducts{name}}","variables":{}}), - None, - None, - ) + self.execute_query(Query::builder().build()) } - #[allow(dead_code)] pub fn execute_query( &self, - query: &Value, - ) -> impl std::future::Future { - self.execute_query_internal(query, None, None) - } - - #[allow(dead_code)] - pub fn execute_bad_query( - &self, - ) -> impl std::future::Future { - self.execute_query_internal(&json!({"garbage":{}}), None, None) - } - - #[allow(dead_code)] - pub fn execute_huge_query( - &self, - ) -> impl std::future::Future { - self.execute_query_internal(&json!({"query":"query {topProducts{name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name}}","variables":{}}), None, None) - } - - #[allow(dead_code)] - pub fn execute_bad_content_type( - &self, - ) -> impl std::future::Future { - self.execute_query_internal(&json!({"garbage":{}}), Some("garbage"), None) - } - - #[allow(dead_code)] - pub fn execute_query_with_headers( - &self, - query: &Value, - headers: HashMap, - ) -> impl std::future::Future { - self.execute_query_internal(query, None, Some(headers)) - } - - fn execute_query_internal( - &self, - query: &Value, - content_type: Option<&'static str>, - headers: Option>, + query: Query, ) -> impl std::future::Future { assert!( self.router.is_some(), @@ -603,37 +638,46 @@ impl IntegrationTest { let telemetry = self.telemetry.clone(); let extra_propagator = self.extra_propagator.clone(); - let query = query.clone(); let url = format!("http://{}", self.bind_address()); - + let subgraph_context = self.subgraph_context.clone(); async move { let span = info_span!("client_request"); - let span_id = span.context().span().span_context().trace_id(); + let trace_id = span.context().span().span_context().trace_id(); async move { let client = reqwest::Client::new(); - let mut builder = client - .post(url) - .header( - CONTENT_TYPE, - content_type.unwrap_or(APPLICATION_JSON.essence_str()), - ) - .header("apollographql-client-name", "custom_name") - .header("apollographql-client-version", "1.0") - .header("x-my-header", "test") - .header("head", "test"); + let mut builder = client.post(url).header(CONTENT_TYPE, query.content_type); - if let Some(headers) = headers { - for (name, value) in headers { - builder = builder.header(name, value); - } + for (name, value) in query.headers { + builder = builder.header(name, value); + } + + if let Some(psr) = query.psr { + builder = builder.header("x-datadog-sampling-priority", psr); + } + + let mut request = builder.json(&query.body).build().unwrap(); + if query.traced { + telemetry.inject_context(&mut request); + extra_propagator.inject_context(&mut request); } - let mut request = builder.json(&query).build().unwrap(); - telemetry.inject_context(&mut request); - extra_propagator.inject_context(&mut request); match client.execute(request).await { - Ok(response) => (span_id, response), + Ok(response) => { + if query.traced { + (trace_id, response) + } else { + ( + subgraph_context + .lock() + .expect("poisoned") + .as_ref() + .expect("subgraph context") + .trace_id(), + response, + ) + } + } Err(err) => { panic!("unable to send successful request to router, {err}") } @@ -645,57 +689,6 @@ impl IntegrationTest { .with_subscriber(self.subscriber_client.clone()) } - #[allow(dead_code)] - pub fn execute_untraced_query( - &self, - query: &Value, - headers: Option>, - ) -> impl std::future::Future { - assert!( - self.router.is_some(), - "router was not started, call `router.start().await; router.assert_started().await`" - ); - let query = query.clone(); - let url = format!("http://{}", self.bind_address()); - - async move { - let client = reqwest::Client::new(); - - let mut builder = client - .post(url) - .header(CONTENT_TYPE, APPLICATION_JSON.essence_str()) - .header("apollographql-client-name", "custom_name") - .header("apollographql-client-version", "1.0") - .json(&query); - - if let Some(headers) = headers { - for (name, value) in headers { - builder = builder.header(name, value); - } - } - - match client.execute(builder.build().unwrap()).await { - Ok(response) => ( - TraceId::from_hex( - response - .headers() - .get("apollo-custom-trace-id") - .cloned() - .unwrap_or(HeaderValue::from_static("no-trace-id")) - .to_str() - .unwrap_or_default(), - ) - .unwrap_or(TraceId::INVALID), - response, - ), - Err(err) => { - panic!("unable to send successful request to router, {err}") - } - } - } - .with_subscriber(self.subscriber_client.clone()) - } - /// Make a raw multipart request to the router. #[allow(dead_code)] pub fn execute_multipart_request( diff --git a/apollo-router/tests/integration/batching.rs b/apollo-router/tests/integration/batching.rs index 15dfd38de2..f9e7ba6ab8 100644 --- a/apollo-router/tests/integration/batching.rs +++ b/apollo-router/tests/integration/batching.rs @@ -856,7 +856,7 @@ mod helper { use wiremock::ResponseTemplate; use super::test_is_enabled; - use crate::integration::common::IntegrationTest; + use crate::integration::common::{IntegrationTest, Query}; /// Helper type for specifying a valid handler pub type Handler = fn(&wiremock::Request) -> ResponseTemplate; @@ -916,7 +916,7 @@ mod helper { // Execute the request let request = serde_json::to_value(requests)?; - let (_span, response) = router.execute_query(&request).await; + let (_span, response) = router.execute_query(Query::builder().body(request).build()).await; serde_json::from_slice::>(&response.bytes().await?).map_err(BoxError::from) } diff --git a/apollo-router/tests/integration/coprocessor.rs b/apollo-router/tests/integration/coprocessor.rs index d9ce741892..21c5f0db2b 100644 --- a/apollo-router/tests/integration/coprocessor.rs +++ b/apollo-router/tests/integration/coprocessor.rs @@ -7,7 +7,7 @@ use wiremock::matchers::path; use wiremock::Mock; use wiremock::ResponseTemplate; -use crate::integration::common::graph_os_enabled; +use crate::integration::common::{graph_os_enabled, Query}; use crate::integration::IntegrationTest; #[tokio::test(flavor = "multi_thread")] @@ -75,7 +75,7 @@ async fn test_coprocessor_limit_payload() -> Result<(), BoxError> { assert_eq!(response.status(), 200); // This query is huge and will be rejected because it is too large before hitting the coprocessor - let (_trace_id, response) = router.execute_huge_query().await; + let (_trace_id, response) = router.execute_query(Query::default().with_huge_query()).await; assert_eq!(response.status(), 413); assert_yaml_snapshot!(response.text().await?); diff --git a/apollo-router/tests/integration/introspection.rs b/apollo-router/tests/integration/introspection.rs index 95c8ad9c8c..56b2a496cd 100644 --- a/apollo-router/tests/integration/introspection.rs +++ b/apollo-router/tests/integration/introspection.rs @@ -1,10 +1,10 @@ +use crate::integration::common::Query; +use crate::integration::IntegrationTest; use apollo_router::plugin::test::MockSubgraph; use apollo_router::services::supergraph::Request; use serde_json::json; use tower::ServiceExt; -use crate::integration::IntegrationTest; - #[tokio::test] async fn simple() { let request = Request::fake_builder() @@ -226,7 +226,9 @@ async fn integration() { let query = json!({ "query": include_str!("../fixtures/introspect_full_schema.graphql"), }); - let (_trace_id, response) = router.execute_query(&query).await; + let (_trace_id, response) = router + .execute_query(Query::builder().body(query).build()) + .await; insta::assert_json_snapshot!(response.json::().await.unwrap()); router.graceful_shutdown().await; } diff --git a/apollo-router/tests/integration/operation_limits.rs b/apollo-router/tests/integration/operation_limits.rs index 79ad7d9f89..b0c5b25802 100644 --- a/apollo-router/tests/integration/operation_limits.rs +++ b/apollo-router/tests/integration/operation_limits.rs @@ -9,7 +9,7 @@ use apollo_router::TestHarness; use serde_json::json; use tower::BoxError; use tower::ServiceExt; - +use crate::integration::common::Query; use crate::integration::IntegrationTest; #[tokio::test(flavor = "multi_thread")] @@ -310,7 +310,7 @@ async fn test_request_bytes_limit_with_coprocessor() -> Result<(), BoxError> { .await; router.start().await; router.assert_started().await; - let (_, resp) = router.execute_huge_query().await; + let (_, resp) = router.execute_query(Query::default().with_huge_query()).await; assert_eq!(resp.status(), 413); router.graceful_shutdown().await; Ok(()) @@ -324,7 +324,7 @@ async fn test_request_bytes_limit() -> Result<(), BoxError> { .await; router.start().await; router.assert_started().await; - let (_, resp) = router.execute_huge_query().await; + let (_, resp) = router.execute_query(Query::default().with_huge_query()).await; assert_eq!(resp.status(), 413); router.graceful_shutdown().await; Ok(()) diff --git a/apollo-router/tests/integration/query_planner/max_evaluated_plans.rs b/apollo-router/tests/integration/query_planner/max_evaluated_plans.rs index d6474aa30b..dc7800feea 100644 --- a/apollo-router/tests/integration/query_planner/max_evaluated_plans.rs +++ b/apollo-router/tests/integration/query_planner/max_evaluated_plans.rs @@ -1,5 +1,5 @@ use serde_json::json; - +use crate::integration::common::Query; use crate::integration::IntegrationTest; fn assert_evaluated_plans(prom: &str, expected: u64) { @@ -31,10 +31,10 @@ async fn reports_evaluated_plans() { router.start().await; router.assert_started().await; router - .execute_query(&json!({ + .execute_query(Query::builder().body(json!({ "query": r#"{ t { v1 v2 v3 v4 } }"#, "variables": {}, - })) + })).build()) .await; let metrics = router @@ -71,10 +71,10 @@ async fn does_not_exceed_max_evaluated_plans_legacy() { router.start().await; router.assert_started().await; router - .execute_query(&json!({ + .execute_query(Query::builder().body(json!({ "query": r#"{ t { v1 v2 v3 v4 } }"#, "variables": {}, - })) + })).build()) .await; let metrics = router @@ -111,10 +111,10 @@ async fn does_not_exceed_max_evaluated_plans() { router.start().await; router.assert_started().await; router - .execute_query(&json!({ + .execute_query(Query::builder().body(json!({ "query": r#"{ t { v1 v2 v3 v4 } }"#, "variables": {}, - })) + })).build()) .await; let metrics = router diff --git a/apollo-router/tests/integration/subgraph_response.rs b/apollo-router/tests/integration/subgraph_response.rs index e37a0da067..802bfc2de4 100644 --- a/apollo-router/tests/integration/subgraph_response.rs +++ b/apollo-router/tests/integration/subgraph_response.rs @@ -1,7 +1,7 @@ use serde_json::json; use tower::BoxError; use wiremock::ResponseTemplate; - +use crate::integration::common::Query; use crate::integration::IntegrationTest; const CONFIG: &str = r#" @@ -21,7 +21,7 @@ async fn test_subgraph_returning_data_null() -> Result<(), BoxError> { router.assert_started().await; let query = "{ __typename topProducts { name } }"; - let (_trace_id, response) = router.execute_query(&json!({ "query": query })).await; + let (_trace_id, response) = router.execute_query(Query::builder().body(json!({ "query": query })).build()).await; assert_eq!(response.status(), 200); assert_eq!( response.json::().await?, @@ -64,7 +64,7 @@ async fn test_subgraph_returning_different_typename_on_query_root() -> Result<() inside_fragment: __typename } "#; - let (_trace_id, response) = router.execute_query(&json!({ "query": query })).await; + let (_trace_id, response) = router.execute_query(Query::builder().body(json!({ "query": query })).build()).await; assert_eq!(response.status(), 200); assert_eq!( response.json::().await?, @@ -99,7 +99,7 @@ async fn test_valid_extensions_service_for_subgraph_error() -> Result<(), BoxErr router.assert_started().await; let (_trace_id, response) = router - .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -141,7 +141,7 @@ async fn test_valid_extensions_service_is_preserved_for_subgraph_error() -> Resu router.assert_started().await; let (_trace_id, response) = router - .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -174,7 +174,7 @@ async fn test_valid_extensions_service_for_invalid_subgraph_response() -> Result router.assert_started().await; let (_trace_id, response) = router - .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -222,7 +222,7 @@ async fn test_valid_error_locations() -> Result<(), BoxError> { router.assert_started().await; let (_trace_id, response) = router - .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -264,7 +264,7 @@ async fn test_empty_error_locations() -> Result<(), BoxError> { router.assert_started().await; let (_trace_id, response) = router - .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -302,7 +302,7 @@ async fn test_invalid_error_locations() -> Result<(), BoxError> { router.assert_started().await; let (_trace_id, response) = router - .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -345,7 +345,7 @@ async fn test_invalid_error_locations_with_single_negative_one_location() -> Res router.assert_started().await; let (_trace_id, response) = router - .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -387,7 +387,7 @@ async fn test_invalid_error_locations_contains_negative_one_location() -> Result router.assert_started().await; let (_trace_id, response) = router - .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) .await; assert_eq!(response.status(), 200); assert_eq!( diff --git a/apollo-router/tests/integration/subscription.rs b/apollo-router/tests/integration/subscription.rs index 911503593f..74c42f2034 100644 --- a/apollo-router/tests/integration/subscription.rs +++ b/apollo-router/tests/integration/subscription.rs @@ -4,7 +4,7 @@ use http::HeaderValue; use serde_json::json; use tower::BoxError; -use super::common::IntegrationTest; +use super::common::{IntegrationTest, Query}; use super::common::Telemetry; const SUBSCRIPTION_CONFIG: &str = include_str!("../fixtures/subscription.router.yaml"); @@ -59,8 +59,7 @@ async fn test_subscription_load() -> Result<(), BoxError> { for _ in 0..100 { let (_id, resp) = router - .execute_query( - &json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}), + .execute_query(Query::builder().body(json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}})).build(), ) .await; assert!(resp.status().is_success()); diff --git a/apollo-router/tests/integration/supergraph.rs b/apollo-router/tests/integration/supergraph.rs index 97d5131d84..5732b0921d 100644 --- a/apollo-router/tests/integration/supergraph.rs +++ b/apollo-router/tests/integration/supergraph.rs @@ -1,8 +1,7 @@ -use std::collections::HashMap; use serde_json::json; use tower::BoxError; - +use crate::integration::common::Query; use crate::integration::IntegrationTest; #[cfg(not(feature = "hyper_header_limits"))] @@ -100,11 +99,8 @@ async fn test_supergraph_errors_on_http1_header_that_does_not_fit_inside_buffer( router.start().await; router.assert_started().await; - let mut headers = HashMap::new(); - headers.insert("test-header".to_string(), "x".repeat(1048576 + 1)); - let (_trace_id, response) = router - .execute_query_with_headers(&json!({ "query": "{ __typename }"}), headers) + .execute_query(Query::builder().body(json!({ "query": "{ __typename }"})).header("test-header", "x".repeat(1048576 + 1)).build()) .await; assert_eq!(response.status(), 431); Ok(()) @@ -125,11 +121,8 @@ async fn test_supergraph_allow_to_change_http1_max_buf_size() -> Result<(), BoxE router.start().await; router.assert_started().await; - let mut headers = HashMap::new(); - headers.insert("test-header".to_string(), "x".repeat(1048576 + 1)); - let (_trace_id, response) = router - .execute_query_with_headers(&json!({ "query": "{ __typename }"}), headers) + .execute_query(Query::builder().body(json!({ "query": "{ __typename }"})).header("test-header", "x".repeat(1048576 + 1)).build()) .await; assert_eq!(response.status(), 200); assert_eq!( diff --git a/apollo-router/tests/integration/telemetry/datadog.rs b/apollo-router/tests/integration/telemetry/datadog.rs index 948ff9d165..591a2702fc 100644 --- a/apollo-router/tests/integration/telemetry/datadog.rs +++ b/apollo-router/tests/integration/telemetry/datadog.rs @@ -1,68 +1,43 @@ extern crate core; -use std::collections::HashMap; use std::collections::HashSet; -use std::sync::Arc; -use std::sync::Mutex; -use std::time::Duration; +use std::ops::Deref; use anyhow::anyhow; -use opentelemetry_api::trace::SpanContext; use opentelemetry_api::trace::TraceId; -use serde_json::json; use serde_json::Value; use tower::BoxError; -use wiremock::ResponseTemplate; use crate::integration::common::graph_os_enabled; +use crate::integration::common::Query; use crate::integration::common::Telemetry; +use crate::integration::telemetry::verifier::Verifier; +use crate::integration::telemetry::TraceSpec; use crate::integration::IntegrationTest; use crate::integration::ValueExt; -#[derive(buildstructor::Builder)] -struct TraceSpec { - operation_name: Option, - version: Option, - services: HashSet<&'static str>, - span_names: HashSet<&'static str>, - measured_spans: HashSet<&'static str>, - unmeasured_spans: HashSet<&'static str>, - priority_sampled: Option<&'static str>, - subgraph_sampled: Option, - subgraph_context: Option>>>, - // Not the metrics but the otel attribute - no_priority_sampled_attribute: Option, -} - #[tokio::test(flavor = "multi_thread")] async fn test_no_sample() -> Result<(), BoxError> { if !graph_os_enabled() { return Ok(()); } - let context = std::sync::Arc::new(std::sync::Mutex::new(None)); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Datadog) .config(include_str!("fixtures/datadog_no_sample.router.yaml")) - .responder(ResponseTemplate::new(200).set_body_json( - json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), - )) - .subgraph_context(context.clone()) .build() .await; router.start().await; router.assert_started().await; + TraceSpec::builder() + .services(["router"].into()) + .subgraph_sampled(false) + .priority_sampled("0") + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(false).build()) + .await?; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (_id, result) = router.execute_untraced_query(&query, None).await; router.graceful_shutdown().await; - assert!(result.status().is_success()); - let context = context.lock().expect("poisoned"); - assert!(!context.as_ref().unwrap().is_sampled()); - assert_eq!( - context.as_ref().unwrap().trace_state().get("psr"), - Some("0") - ); Ok(()) } @@ -73,34 +48,25 @@ async fn test_sampling_datadog_agent_disabled() -> Result<(), BoxError> { if !graph_os_enabled() { return Ok(()); } - let context = std::sync::Arc::new(std::sync::Mutex::new(None)); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Datadog) .config(include_str!( "fixtures/datadog_agent_sampling_disabled.router.yaml" )) - .responder(ResponseTemplate::new(200).set_body_json( - json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), - )) - .subgraph_context(context.clone()) .build() .await; router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_untraced_query(&query, None).await; - router.graceful_shutdown().await; - assert!(result.status().is_success()); - tokio::time::sleep(Duration::from_secs(2)).await; - TraceSpec::builder() .services([].into()) .subgraph_sampled(false) .build() - .validate_trace(id) + .validate_datadog_trace(&mut router, Query::builder().traced(false).build()) .await?; + router.graceful_shutdown().await; + Ok(()) } @@ -109,14 +75,9 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { if !graph_os_enabled() { return Ok(()); } - let context = std::sync::Arc::new(std::sync::Mutex::new(None)); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Datadog) .config(include_str!("fixtures/datadog.router.yaml")) - .responder(ResponseTemplate::new(200).set_body_json( - json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), - )) - .subgraph_context(context.clone()) .build() .await; @@ -124,58 +85,46 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { router.assert_started().await; // Parent based sampling. psr MUST be populated with the value that we pass in. - test_psr( - &mut router, - Some("-1"), - TraceSpec::builder() - .services(["client", "router"].into()) - .subgraph_sampled(false) - .priority_sampled("-1") - .build(), - ) - .await?; - test_psr( - &mut router, - Some("0"), - TraceSpec::builder() - .services(["client", "router"].into()) - .subgraph_sampled(false) - .priority_sampled("0") - .build(), - ) - .await?; - test_psr( - &mut router, - Some("1"), - TraceSpec::builder() - .services(["client", "router", "subgraph"].into()) - .subgraph_sampled(true) - .priority_sampled("1") - .build(), - ) - .await?; - test_psr( - &mut router, - Some("2"), - TraceSpec::builder() - .services(["client", "router", "subgraph"].into()) - .subgraph_sampled(true) - .priority_sampled("2") - .build(), - ) - .await?; + TraceSpec::builder() + .services(["client", "router"].into()) + .subgraph_sampled(false) + .priority_sampled("-1") + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).psr("-1").build()) + .await?; + + TraceSpec::builder() + .services(["client", "router"].into()) + .subgraph_sampled(false) + .priority_sampled("0") + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).psr("0").build()) + .await?; + + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .subgraph_sampled(true) + .priority_sampled("1") + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).psr("1").build()) + .await?; + + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .subgraph_sampled(true) + .priority_sampled("2") + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).psr("2").build()) + .await?; // No psr was passed in the router is free to set it. This will be 1 as we are going to sample here. - test_psr( - &mut router, - None, - TraceSpec::builder() - .services(["client", "router", "subgraph"].into()) - .subgraph_sampled(true) - .priority_sampled("1") - .build(), - ) - .await?; + TraceSpec::builder() + .services(["router", "subgraph"].into()) + .subgraph_sampled(true) + .priority_sampled("1") + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(false).build()) + .await?; router.graceful_shutdown().await; @@ -187,35 +136,22 @@ async fn test_priority_sampling_propagated_otel_request() -> Result<(), BoxError if !graph_os_enabled() { return Ok(()); } - let context = std::sync::Arc::new(std::sync::Mutex::new(None)); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Otlp { endpoint: None }) + .extra_propagator(Telemetry::Datadog) .config(include_str!("fixtures/datadog.router.yaml")) - .responder(ResponseTemplate::new(200).set_body_json( - json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), - )) - .subgraph_context(context.clone()) .build() .await; router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_query(&query).await; - assert_eq!( - result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .to_str() - .unwrap(), - id.to_datadog() - ); + TraceSpec::builder() .services(["router"].into()) .priority_sampled("1") + .subgraph_sampled(true) .build() - .validate_trace(id) + .validate_datadog_trace(&mut router, Query::builder().traced(true).build()) .await?; router.graceful_shutdown().await; @@ -228,16 +164,11 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { if !graph_os_enabled() { return Ok(()); } - let context = std::sync::Arc::new(std::sync::Mutex::new(None)); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Datadog) .config(include_str!( "fixtures/datadog_no_parent_sampler.router.yaml" )) - .responder(ResponseTemplate::new(200).set_body_json( - json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), - )) - .subgraph_context(context.clone()) .build() .await; @@ -245,74 +176,141 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { router.assert_started().await; // The router will ignore the upstream PSR as parent based sampling is disabled. - test_psr( - &mut router, - Some("-1"), - TraceSpec::builder() - .services(["client", "router", "subgraph"].into()) - .priority_sampled("1") - .build(), - ) - .await?; - test_psr( - &mut router, - Some("0"), - TraceSpec::builder() - .services(["client", "router", "subgraph"].into()) - .priority_sampled("1") - .build(), - ) - .await?; - test_psr( - &mut router, - Some("1"), - TraceSpec::builder() - .services(["client", "router", "subgraph"].into()) - .priority_sampled("1") - .build(), - ) - .await?; - test_psr( - &mut router, - Some("2"), - TraceSpec::builder() - .services(["client", "router", "subgraph"].into()) - .priority_sampled("1") - .build(), - ) - .await?; - - test_psr( - &mut router, - None, - TraceSpec::builder() - .services(["client", "router", "subgraph"].into()) - .priority_sampled("1") - .build(), - ) - .await?; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).psr("-1").build()) + .await?; + + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).psr("0").build()) + .await?; + + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).psr("1").build()) + .await?; + + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).psr("2").build()) + .await?; + + TraceSpec::builder() + .services(["router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(false).build()) + .await?; router.graceful_shutdown().await; Ok(()) } -async fn test_psr( - router: &mut IntegrationTest, - psr: Option<&str>, - trace_spec: TraceSpec, -) -> Result<(), BoxError> { - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let headers = if let Some(psr) = psr { - vec![("x-datadog-sampling-priority".to_string(), psr.to_string())] - } else { - vec![] - }; - let (id, result) = router - .execute_query_with_headers(&query, headers.into_iter().collect()) +#[tokio::test(flavor = "multi_thread")] +async fn test_priority_sampling_parent_sampler_very_small() -> Result<(), BoxError> { + // Note that there is a very small chance this test will fail. We are trying to test a non-zero sampler. + + if !graph_os_enabled() { + return Ok(()); + } + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Datadog) + .config(include_str!( + "fixtures/datadog_parent_sampler_very_small.router.yaml" + )) + .build() + .await; + + router.start().await; + router.assert_started().await; + + // The router should respect upstream but also almost never sample if left to its own devices. + TraceSpec::builder() + .services(["client", "router"].into()) + .priority_sampled("-1") + .subgraph_sampled(false) + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).psr("-1").build()) + .await?; + + TraceSpec::builder() + .services(["client", "router"].into()) + .priority_sampled("0") + .subgraph_sampled(false) + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).psr("0").build()) + .await?; + + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).psr("1").build()) + .await?; + + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("2") + .subgraph_sampled(true) + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).psr("2").build()) + .await?; + + TraceSpec::builder() + .services(["router"].into()) + .priority_sampled("0") + .subgraph_sampled(false) + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(false).build()) + .await?; + + router.graceful_shutdown().await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_untraced_request() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Datadog) + .config(include_str!( + "fixtures/datadog_parent_sampler_very_small.router.yaml" + )) + .build() .await; - assert!(result.status().is_success()); - trace_spec.validate_trace(id).await?; + + router.start().await; + router.assert_started().await; + + TraceSpec::builder() + .services(["router"].into()) + .priority_sampled("0") + .subgraph_sampled(false) + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(false).build()) + .await?; + + router.graceful_shutdown().await; + Ok(()) } @@ -332,20 +330,9 @@ async fn test_default_span_names() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_query(&query).await; - assert_eq!( - result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .to_str() - .unwrap(), - id.to_datadog() - ); - router.graceful_shutdown().await; TraceSpec::builder() .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") .span_names( [ "query_planning", @@ -363,8 +350,9 @@ async fn test_default_span_names() -> Result<(), BoxError> { .into(), ) .build() - .validate_trace(id) + .validate_datadog_trace(&mut router, Query::builder().traced(true).build()) .await?; + router.graceful_shutdown().await; Ok(()) } @@ -384,20 +372,9 @@ async fn test_override_span_names() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_query(&query).await; - assert_eq!( - result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .to_str() - .unwrap(), - id.to_datadog() - ); - router.graceful_shutdown().await; TraceSpec::builder() .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") .span_names( [ "query_planning", @@ -415,8 +392,9 @@ async fn test_override_span_names() -> Result<(), BoxError> { .into(), ) .build() - .validate_trace(id) + .validate_datadog_trace(&mut router, Query::builder().traced(true).build()) .await?; + router.graceful_shutdown().await; Ok(()) } @@ -435,21 +413,9 @@ async fn test_override_span_names_late() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_query(&query).await; - assert_eq!( - result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .to_str() - .unwrap(), - id.to_datadog() - ); - router.graceful_shutdown().await; TraceSpec::builder() .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") .span_names( [ "query_planning", @@ -467,8 +433,9 @@ async fn test_override_span_names_late() -> Result<(), BoxError> { .into(), ) .build() - .validate_trace(id) + .validate_datadog_trace(&mut router, Query::builder().traced(true).build()) .await?; + router.graceful_shutdown().await; Ok(()) } @@ -486,20 +453,9 @@ async fn test_basic() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_query(&query).await; - assert_eq!( - result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .to_str() - .unwrap(), - id.to_datadog() - ); - router.graceful_shutdown().await; TraceSpec::builder() .operation_name("ExampleQuery") + .priority_sampled("1") .services(["client", "router", "subgraph"].into()) .span_names( [ @@ -530,8 +486,9 @@ async fn test_basic() -> Result<(), BoxError> { .into(), ) .build() - .validate_trace(id) + .validate_datadog_trace(&mut router, Query::builder().traced(true).build()) .await?; + router.graceful_shutdown().await; Ok(()) } @@ -549,23 +506,6 @@ async fn test_with_parent_span() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let mut headers = HashMap::new(); - headers.insert( - "traceparent".to_string(), - String::from("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"), - ); - let (id, result) = router.execute_query_with_headers(&query, headers).await; - assert_eq!( - result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .to_str() - .unwrap(), - id.to_datadog() - ); - router.graceful_shutdown().await; TraceSpec::builder() .operation_name("ExampleQuery") .services(["client", "router", "subgraph"].into()) @@ -598,8 +538,18 @@ async fn test_with_parent_span() -> Result<(), BoxError> { .into(), ) .build() - .validate_trace(id) + .validate_datadog_trace( + &mut router, + Query::builder() + .traced(true) + .header( + "traceparent", + "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", + ) + .build(), + ) .await?; + router.graceful_shutdown().await; Ok(()) } @@ -619,13 +569,6 @@ async fn test_resource_mapping_default() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_query(&query).await; - assert!(!result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .is_empty()); TraceSpec::builder() .operation_name("ExampleQuery") .services(["client", "router", "subgraph"].into()) @@ -645,7 +588,7 @@ async fn test_resource_mapping_default() -> Result<(), BoxError> { .into(), ) .build() - .validate_trace(id) + .validate_datadog_trace(&mut router, Query::builder().traced(true).build()) .await?; router.graceful_shutdown().await; Ok(()) @@ -667,14 +610,6 @@ async fn test_resource_mapping_override() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_query(&query).await; - assert!(!result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .is_empty()); - router.graceful_shutdown().await; TraceSpec::builder() .services(["client", "router", "subgraph"].into()) .span_names( @@ -693,8 +628,9 @@ async fn test_resource_mapping_override() -> Result<(), BoxError> { .into(), ) .build() - .validate_trace(id) + .validate_datadog_trace(&mut router, Query::builder().traced(true).build()) .await?; + router.graceful_shutdown().await; Ok(()) } @@ -712,14 +648,6 @@ async fn test_span_metrics() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_query(&query).await; - assert!(!result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .is_empty()); - router.graceful_shutdown().await; TraceSpec::builder() .operation_name("ExampleQuery") .services(["client", "router", "subgraph"].into()) @@ -740,8 +668,9 @@ async fn test_span_metrics() -> Result<(), BoxError> { .measured_span("subgraph") .unmeasured_span("supergraph") .build() - .validate_trace(id) + .validate_datadog_trace(&mut router, Query::builder().traced(true).build()) .await?; + router.graceful_shutdown().await; Ok(()) } @@ -755,58 +684,33 @@ impl DatadogId for TraceId { } } -impl TraceSpec { - #[allow(clippy::too_many_arguments)] - async fn validate_trace(&self, id: TraceId) -> Result<(), BoxError> { - let datadog_id = id.to_datadog(); - let url = format!("http://localhost:8126/test/traces?trace_ids={datadog_id}"); - for _ in 0..20 { - if self.find_valid_trace(&url).await.is_ok() { - return Ok(()); - } - tokio::time::sleep(Duration::from_millis(100)).await; - } - self.find_valid_trace(&url).await?; - - if let Some(subgraph_context) = &self.subgraph_context { - let subgraph_context = subgraph_context.lock().expect("poisoned"); - let subgraph_span_context = subgraph_context.as_ref().expect("state").clone(); +struct DatadogTraceSpec { + trace_spec: TraceSpec, +} +impl Deref for DatadogTraceSpec { + type Target = TraceSpec; - assert_eq!( - subgraph_span_context.trace_state().get("psr"), - self.priority_sampled - ); - if let Some(sampled) = self.subgraph_sampled { - assert_eq!(subgraph_span_context.is_sampled(), sampled); - } - } - Ok(()) + fn deref(&self) -> &Self::Target { + &self.trace_spec } +} - #[allow(clippy::too_many_arguments)] - async fn find_valid_trace(&self, url: &str) -> Result<(), BoxError> { - // A valid trace has: - // * All three services - // * The correct spans - // * All spans are parented - // * Required attributes of 'router' span has been set +impl Verifier for DatadogTraceSpec { + fn spec(&self) -> &TraceSpec { + &self.trace_spec + } - // For now just validate service name. - let trace: Value = reqwest::get(url) + async fn get_trace(&self, trace_id: TraceId) -> Result { + let datadog_id = trace_id.to_datadog(); + let url = format!("http://localhost:8126/test/traces?trace_ids={datadog_id}"); + println!("url: {}", url); + let value: serde_json::Value = reqwest::get(url) .await .map_err(|e| anyhow!("failed to contact datadog; {}", e))? .json() - .await?; - tracing::debug!("{}", serde_json::to_string_pretty(&trace)?); - self.verify_trace_participants(&trace)?; - self.verify_spans_present(&trace)?; - self.verify_measured_spans(&trace)?; - self.verify_operation_name(&trace)?; - self.verify_priority_sampled(&trace)?; - self.verify_priority_sampled_attribute(&trace)?; - self.verify_version(&trace)?; - self.verify_span_kinds(&trace)?; - Ok(()) + .await + .map_err(|e| anyhow!("failed to contact datadog; {}", e))?; + Ok(value) } fn verify_version(&self, trace: &Value) -> Result<(), BoxError> { @@ -824,24 +728,6 @@ impl TraceSpec { Ok(()) } - fn verify_measured_spans(&self, trace: &Value) -> Result<(), BoxError> { - for expected in &self.measured_spans { - assert!( - self.measured_span(trace, expected)?, - "missing measured span {}", - expected - ); - } - for unexpected in &self.unmeasured_spans { - assert!( - !self.measured_span(trace, unexpected)?, - "unexpected measured span {}", - unexpected - ); - } - Ok(()) - } - fn measured_span(&self, trace: &Value, name: &str) -> Result { let binding1 = trace.select_path(&format!( "$..[?(@.meta.['otel.original_name'] == '{}')].metrics.['_dd.measured']", @@ -859,17 +745,7 @@ impl TraceSpec { .unwrap_or_default()) } - fn verify_span_kinds(&self, trace: &Value) -> Result<(), BoxError> { - // Validate that the span.kind has been propagated. We can just do this for a selection of spans. - if self.services.contains("router") { - self.validate_span_kind(trace, "router", "server")?; - self.validate_span_kind(trace, "supergraph", "internal")?; - self.validate_span_kind(trace, "http_request", "client")?; - } - Ok(()) - } - - fn verify_trace_participants(&self, trace: &Value) -> Result<(), BoxError> { + fn verify_services(&self, trace: &Value) -> Result<(), BoxError> { let actual_services: HashSet = trace .select_path("$..service")? .into_iter() @@ -897,7 +773,7 @@ impl TraceSpec { .filter_map(|span_name| span_name.as_string()) .collect(); let mut span_names: HashSet<&str> = self.span_names.clone(); - if self.services.contains("client") { + if self.services.contains(&"client") { span_names.insert("client_request"); } tracing::debug!("found spans {:?}", operation_names); @@ -914,6 +790,7 @@ impl TraceSpec { } fn validate_span_kind(&self, trace: &Value, name: &str, kind: &str) -> Result<(), BoxError> { + let binding1 = trace.select_path(&format!( "$..[?(@.meta.['otel.original_name'] == '{}')].meta.['span.kind']", name @@ -979,16 +856,19 @@ impl TraceSpec { Ok(()) } - fn verify_priority_sampled_attribute(&self, trace: &Value) -> Result<(), BoxError> { - if self.no_priority_sampled_attribute.unwrap_or_default() { - let binding = - trace.select_path("$..[?(@.service=='router')].meta['sampling.priority']")?; - if binding.is_empty() { - return Ok(()); - } else { - return Err(BoxError::from("sampling priority attribute exists")); - } - } + fn verify_span_attributes(&self, _trace: &Value) -> Result<(), BoxError> { Ok(()) } } + +impl TraceSpec { + async fn validate_datadog_trace( + self, + router: &mut IntegrationTest, + query: Query, + ) -> Result<(), BoxError> { + DatadogTraceSpec { trace_spec: self } + .validate_trace(router, query) + .await + } +} diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_parent_sampler_very_small.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_parent_sampler_very_small.router.yaml new file mode 100644 index 0000000000..90a5594503 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_parent_sampler_very_small.router.yaml @@ -0,0 +1,29 @@ +telemetry: + exporters: + tracing: + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + format: datadog + propagation: + trace_context: true + jaeger: true + common: + service_name: router + sampler: 0.000000001 + parent_based_sampler: true + resource: + env: local1 + service.version: router_version_override + preview_datadog_agent_sampling: true + datadog: + enabled: true + batch_processor: + scheduled_delay: 100ms + instrumentation: + spans: + mode: spec_compliant + supergraph: + attributes: + graphql.operation.name: true + diff --git a/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_request_with_zipkin_propagator.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_request_with_zipkin_propagator.router.yaml index 4e31e0d1d6..3bcb4e5db5 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_request_with_zipkin_propagator.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/otlp_datadog_request_with_zipkin_propagator.router.yaml @@ -5,6 +5,7 @@ telemetry: tracing: propagation: zipkin: true + datadog: true trace_context: true common: service_name: router diff --git a/apollo-router/tests/integration/telemetry/jaeger.rs b/apollo-router/tests/integration/telemetry/jaeger.rs index c9e79bd22a..2c22ef4fc5 100644 --- a/apollo-router/tests/integration/telemetry/jaeger.rs +++ b/apollo-router/tests/integration/telemetry/jaeger.rs @@ -1,7 +1,7 @@ extern crate core; use std::collections::HashSet; -use std::time::Duration; +use std::ops::Deref; use anyhow::anyhow; use opentelemetry_api::trace::TraceId; @@ -9,7 +9,9 @@ use serde_json::json; use serde_json::Value; use tower::BoxError; -use crate::integration::common::Telemetry; +use crate::integration::common::{Query, Telemetry}; +use crate::integration::telemetry::verifier::Verifier; +use crate::integration::telemetry::TraceSpec; use crate::integration::IntegrationTest; use crate::integration::ValueExt; @@ -24,22 +26,13 @@ async fn test_reload() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); for _ in 0..2 { - let (id, result) = router.execute_query(&query).await; - assert!(!result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .is_empty()); - validate_trace( - id, - &query, - Some("ExampleQuery"), - &["client", "router", "subgraph"], - false, - ) - .await?; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .operation_name("ExampleQuery") + .build() + .validate_jaeger_trace(&mut router, Query::default()) + .await?; router.touch_config().await; router.assert_reloaded().await; } @@ -58,21 +51,11 @@ async fn test_remote_root() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_query(&query).await; - assert!(!result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .is_empty()); - validate_trace( - id, - &query, - Some("ExampleQuery"), - &["client", "router", "subgraph"], - false, - ) - .await?; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .build() + .validate_jaeger_trace(&mut router, Query::default()) + .await?; router.graceful_shutdown().await; Ok(()) @@ -89,21 +72,12 @@ async fn test_local_root() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_untraced_query(&query, None).await; - assert!(!result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .is_empty()); - validate_trace( - id, - &query, - Some("ExampleQuery"), - &["router", "subgraph"], - false, - ) - .await?; + TraceSpec::builder() + .services(["router", "subgraph"].into()) + .operation_name("ExampleQuery") + .build() + .validate_jaeger_trace(&mut router, Query::builder().traced(false).build()) + .await?; router.graceful_shutdown().await; Ok(()) @@ -120,8 +94,9 @@ async fn test_local_root_no_sample() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (_, response) = router.execute_untraced_query(&query, None).await; + let (_, response) = router + .execute_query(Query::builder().traced(false).build()) + .await; assert!(response.headers().get("apollo-custom-trace-id").is_some()); router.graceful_shutdown().await; @@ -138,19 +113,13 @@ async fn test_local_root_50_percent_sample() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}\n","variables":{}, "operationName": "ExampleQuery"}); for _ in 0..100 { - let (id, result) = router.execute_untraced_query(&query, None).await; - - if result.headers().get("apollo-custom-trace-id").is_some() - && validate_trace( - id, - &query, - Some("ExampleQuery"), - &["router", "subgraph"], - false, - ) + if TraceSpec::builder() + .services(["router", "subgraph"].into()) + .operation_name("ExampleQuery") + .build() + .validate_jaeger_trace(&mut router, Query::builder().traced(false).build()) .await .is_ok() { @@ -176,10 +145,11 @@ async fn test_no_telemetry() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (_, response) = router.execute_untraced_query(&query, None).await; - assert!(response.headers().get("apollo-custom-trace-id").is_none()); - + TraceSpec::builder() + .services(["router", "subgraph"].into()) + .build() + .validate_jaeger_trace(&mut router, Query::builder().traced(false).build()) + .await?; router.graceful_shutdown().await; Ok(()) } @@ -194,22 +164,11 @@ async fn test_default_operation() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery1 {topProducts{name}}","variables":{}}); - - let (id, result) = router.execute_query(&query).await; - assert!(!result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .is_empty()); - validate_trace( - id, - &query, - Some("ExampleQuery1"), - &["client", "router", "subgraph"], - false, - ) - .await?; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .build() + .validate_jaeger_trace(&mut router, Query::default()) + .await?; router.graceful_shutdown().await; Ok(()) } @@ -225,15 +184,11 @@ async fn test_anonymous_operation() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query {topProducts{name}}","variables":{}}); - - let (id, result) = router.execute_query(&query).await; - assert!(!result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .is_empty()); - validate_trace(id, &query, None, &["client", "router", "subgraph"], false).await?; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .build() + .validate_jaeger_trace(&mut router, Query::builder().build()) + .await?; router.graceful_shutdown().await; Ok(()) } @@ -248,22 +203,15 @@ async fn test_selected_operation() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery1 {topProducts{name}}\nquery ExampleQuery2 {topProducts{name}}","variables":{}, "operationName": "ExampleQuery2"}); - - let (id, result) = router.execute_query(&query).await; - assert!(!result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .is_empty()); - validate_trace( - id, - &query, - Some("ExampleQuery2"), - &["client", "router", "subgraph"], - false, - ) - .await?; + TraceSpec::builder().services(["client", "router", "subgraph"].into()) + .operation_name("ExampleQuery2") + .build() + .validate_jaeger_trace( + &mut router, + Query::builder() + .body(json!({"query":"query ExampleQuery1 {topProducts{name}}\nquery ExampleQuery2 {topProducts{name}}","variables":{}, "operationName": "ExampleQuery2"}) + ).build(), + ).await?; router.graceful_shutdown().await; Ok(()) } @@ -280,16 +228,12 @@ async fn test_span_customization() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, _res) = router.execute_query(&query).await; - validate_trace( - id, - &query, - Some("ExampleQuery"), - &["client", "router", "subgraph"], - true, - ) - .await?; + TraceSpec::builder().services(["client", "router", "subgraph"].into()) + .operation_name("ExampleQuery") + .span_attribute("http.request.method", "POST") + .build() + .validate_jaeger_trace(&mut router, Query::builder().build()) + .await?; router.graceful_shutdown().await; } Ok(()) @@ -305,9 +249,8 @@ async fn test_decimal_trace_id() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery1 {topProducts{name}}","variables":{}}); - let (id, result) = router.execute_query(&query).await; + let (id, result) = router.execute_query(Query::default()).await; let id_from_router: u128 = result .headers() .get("apollo-custom-trace-id") @@ -317,343 +260,191 @@ async fn test_decimal_trace_id() -> Result<(), BoxError> { .parse() .expect("expected decimal trace ID"); assert_eq!(format!("{:x}", id_from_router), id.to_string()); - - validate_trace( - id, - &query, - Some("ExampleQuery1"), - &["client", "router", "subgraph"], - false, - ) - .await?; router.graceful_shutdown().await; Ok(()) } -async fn validate_trace( - id: TraceId, - query: &Value, - operation_name: Option<&str>, - services: &[&'static str], - custom_span_instrumentation: bool, -) -> Result<(), BoxError> { - let params = url::form_urlencoded::Serializer::new(String::new()) - .append_pair("service", services.first().expect("expected root service")) - .finish(); - - let id = id.to_string(); - let url = format!("http://localhost:16686/api/traces/{id}?{params}"); - for _ in 0..10 { - if find_valid_trace( - &url, - query, - operation_name, - services, - custom_span_instrumentation, - ) - .await - .is_ok() - { - return Ok(()); - } - tokio::time::sleep(Duration::from_millis(1000)).await; - } - find_valid_trace( - &url, - query, - operation_name, - services, - custom_span_instrumentation, - ) - .await?; - Ok(()) +struct JaegerTraceSpec { + trace_spec: TraceSpec, } +impl Deref for JaegerTraceSpec { + type Target = TraceSpec; -async fn find_valid_trace( - url: &str, - query: &Value, - operation_name: Option<&str>, - services: &[&'static str], - custom_span_instrumentation: bool, -) -> Result<(), BoxError> { - // A valid trace has: - // * All three services - // * The correct spans - // * All spans are parented - // * Required attributes of 'router' span has been set - let trace: Value = reqwest::get(url) - .await - .map_err(|e| anyhow!("failed to contact jaeger; {}", e))? - .json() - .await?; - tracing::debug!("{}", serde_json::to_string_pretty(&trace)?); - - // Verify that we got all the participants in the trace - verify_trace_participants(&trace, services)?; - - // Verify that we got the expected span operation names - verify_spans_present(&trace, operation_name, services)?; - - // Verify that all spans have a path to the root 'client_request' span - verify_span_parenting(&trace, services)?; + fn deref(&self) -> &Self::Target { + &self.trace_spec + } +} - // Verify that root span fields are present - verify_root_span_fields(&trace, operation_name)?; +impl Verifier for JaegerTraceSpec { + fn spec(&self) -> &TraceSpec { + &self.trace_spec + } - // Verify that supergraph span fields are present - verify_supergraph_span_fields(&trace, query, operation_name, custom_span_instrumentation)?; + fn verify_span_attributes(&self, trace: &Value) -> Result<(), BoxError> { + for (key, value) in &self.span_attributes { + let binding = trace.select_path(&format!( + "$..tags[?(@.key == '{key}')].value"))?; + let actual_value = binding + .first() + .expect("expected binding") + .as_str() + .expect("expected string"); + assert_eq!(actual_value, *value); + } + Ok(()) + } - // Verify that router span fields are present - verify_router_span_fields(&trace, custom_span_instrumentation)?; + async fn get_trace(&self, trace_id: TraceId) -> Result { + let params = url::form_urlencoded::Serializer::new(String::new()) + .append_pair( + "service", + self.trace_spec + .services + .first() + .expect("expected root service"), + ) + .finish(); - Ok(()) -} + let id = trace_id.to_string(); + let url = format!("http://localhost:16686/api/traces/{id}?{params}"); + println!("url: {}", url); + let value: serde_json::Value = reqwest::get(url) + .await + .map_err(|e| anyhow!("failed to contact jaeger; {}", e))? + .json() + .await + .map_err(|e| anyhow!("failed to contact jaeger; {}", e))?; -fn verify_router_span_fields( - trace: &Value, - custom_span_instrumentation: bool, -) -> Result<(), BoxError> { - let router_span = trace.select_path("$..spans[?(@.operationName == 'router')]")?[0]; - // We can't actually assert the values on a span. Only that a field has been set. - assert_eq!( - router_span - .select_path("$.tags[?(@.key == 'client.name')].value")? - .first(), - Some(&&Value::String("custom_name".to_string())) - ); - assert_eq!( - router_span - .select_path("$.tags[?(@.key == 'client.version')].value")? - .first(), - Some(&&Value::String("1.0".to_string())) - ); - assert!(router_span - .select_path("$.logs[*].fields[?(@.key == 'histogram.apollo_router_span')].value")? - .is_empty(),); - assert!(router_span - .select_path("$.logs[*].fields[?(@.key == 'histogram.apollo_router_span')].value")? - .is_empty(),); - if custom_span_instrumentation { - assert_eq!( - router_span - .select_path("$.tags[?(@.key == 'http.request.method')].value")? - .first(), - Some(&&Value::String("POST".to_string())) - ); - assert_eq!( - router_span - .select_path("$.tags[?(@.key == 'http.request.header.x-not-present')].value")? - .first(), - Some(&&Value::String("nope".to_string())) - ); - assert_eq!( - router_span - .select_path( - "$.tags[?(@.key == 'http.request.header.x-my-header-condition')].value" - )? - .first(), - Some(&&Value::String("test".to_string())) - ); - assert_eq!( - router_span - .select_path("$.tags[?(@.key == 'studio.operation.id')].value")? - .first(), - Some(&&Value::String( - "f60e643d7f52ecda23216f86409d7e2e5c3aa68c".to_string() - )) - ); + Ok(value) } - Ok(()) -} - -fn verify_root_span_fields(trace: &Value, operation_name: Option<&str>) -> Result<(), BoxError> { - // We can't actually assert the values on a span. Only that a field has been set. - let root_span_name = operation_name - .map(|name| format!("query {}", name)) - .unwrap_or("query".to_string()); - let request_span = trace.select_path(&format!( - "$..spans[?(@.operationName == '{root_span_name}')]" - ))?[0]; - - if let Some(operation_name) = operation_name { - assert_eq!( - request_span - .select_path("$.tags[?(@.key == 'graphql.operation.name')].value")? - .first(), - Some(&&Value::String(operation_name.to_string())) - ); - } else { - assert!(request_span - .select_path("$.tags[?(@.key == 'graphql.operation.name')].value")? - .first() - .is_none(),); + fn verify_version(&self, trace: &Value) -> Result<(), BoxError> { + if let Some(expected_version) = &self.version { + let binding = trace.select_path("$..version")?; + let version = binding.first(); + assert_eq!( + version + .expect("version expected") + .as_str() + .expect("version must be a string"), + expected_version + ); + } + Ok(()) } - assert_eq!( - request_span - .select_path("$.tags[?(@.key == 'graphql.operation.type')].value")? - .first(), - Some(&&Value::String("query".to_string())) - ); - - Ok(()) -} - -fn verify_supergraph_span_fields( - trace: &Value, - query: &Value, - operation_name: Option<&str>, - custom_span_instrumentation: bool, -) -> Result<(), BoxError> { - // We can't actually assert the values on a span. Only that a field has been set. - let supergraph_span = trace.select_path("$..spans[?(@.operationName == 'supergraph')]")?[0]; - - if let Some(operation_name) = operation_name { - assert_eq!( - supergraph_span - .select_path("$.tags[?(@.key == 'graphql.operation.name')].value")? - .first(), - Some(&&Value::String(operation_name.to_string())) - ); - } else { - assert!(supergraph_span - .select_path("$.tags[?(@.key == 'graphql.operation.name')].value")? + fn measured_span(&self, trace: &Value, name: &str) -> Result { + let binding1 = trace.select_path(&format!( + "$..[?(@.meta.['otel.original_name'] == '{}')].metrics.['_dd.measured']", + name + ))?; + let binding2 = trace.select_path(&format!( + "$..[?(@.name == '{}')].metrics.['_dd.measured']", + name + ))?; + Ok(binding1 .first() - .is_none(),); + .or(binding2.first()) + .and_then(|v| v.as_f64()) + .map(|v| v == 1.0) + .unwrap_or_default()) } - if custom_span_instrumentation { - assert_eq!( - supergraph_span - .select_path("$.tags[?(@.key == 'graphql.operation.type')].value")? - .first(), - Some(&&Value::String("query".to_string())) - ); + + fn verify_services(&self, trace: &Value) -> Result<(), BoxError> { + let actual_services: HashSet = trace + .select_path("$..serviceName")? + .into_iter() + .filter_map(|service| service.as_string()) + .collect(); + tracing::debug!("found services {:?}", actual_services); + + let expected_services = self.trace_spec.services + .iter() + .map(|s| s.to_string()) + .collect::>(); + if actual_services != expected_services { + return Err(BoxError::from(format!( + "incomplete traces, got {actual_services:?} expected {expected_services:?}" + ))); + } + Ok(()) } - assert_eq!( - supergraph_span - .select_path("$.tags[?(@.key == 'graphql.document')].value")? - .first(), - Some(&&Value::String( - query - .as_object() - .expect("should have been an object") - .get("query") - .expect("must have a query") - .as_str() - .expect("must be a string") - .to_string() - )) - ); + fn verify_spans_present(&self, trace: &Value) -> Result<(), BoxError> { - Ok(()) -} + let operation_names: HashSet = trace + .select_path("$..operationName")? + .into_iter() + .filter_map(|span_name| span_name.as_string()) + .collect(); -fn verify_trace_participants(trace: &Value, services: &[&'static str]) -> Result<(), BoxError> { - let actual_services: HashSet = trace - .select_path("$..serviceName")? - .into_iter() - .filter_map(|service| service.as_string()) - .collect(); - tracing::debug!("found services {:?}", actual_services); - - let expected_services = services - .iter() - .map(|s| s.to_string()) - .collect::>(); - if actual_services != expected_services { - return Err(BoxError::from(format!( - "incomplete traces, got {actual_services:?} expected {expected_services:?}" - ))); + let mut span_names: HashSet<&str> = self.span_names.clone(); + if self.services.contains(&"client") { + span_names.insert("client_request"); + } + tracing::debug!("found spans {:?}", operation_names); + let missing_operation_names: Vec<_> = span_names + .iter() + .filter(|o| !operation_names.contains(**o)) + .collect(); + if !missing_operation_names.is_empty() { + return Err(BoxError::from(format!( + "spans did not match, got {operation_names:?}, missing {missing_operation_names:?}" + ))); + } + Ok(()) } - Ok(()) -} -fn verify_spans_present( - trace: &Value, - operation_name: Option<&str>, - services: &[&'static str], -) -> Result<(), BoxError> { - let operation_names: HashSet = trace - .select_path("$..operationName")? - .into_iter() - .filter_map(|span_name| span_name.as_string()) - .collect(); - let mut expected_operation_names: HashSet = HashSet::from( - [ - "execution", - "subgraph server", - operation_name - .map(|name| format!("query {name}")) - .unwrap_or("query".to_string()) - .as_str(), - "supergraph", - "fetch", - //"parse_query", Parse query will only happen once - //"query_planning", query planning will only happen once - "subgraph", - ] - .map(|s| s.into()), - ); - if services.contains(&"client") { - expected_operation_names.insert("client_request".into()); - } - tracing::debug!("found spans {:?}", operation_names); - let missing_operation_names: Vec<_> = expected_operation_names - .iter() - .filter(|o| !operation_names.contains(*o)) - .collect(); - if !missing_operation_names.is_empty() { - return Err(BoxError::from(format!( - "spans did not match, got {operation_names:?}, missing {missing_operation_names:?}" - ))); + fn validate_span_kind(&self, _trace: &Value, _name: &str, _kind: &str) -> Result<(), BoxError> { + Ok(()) } - Ok(()) -} -fn verify_span_parenting(trace: &Value, services: &[&'static str]) -> Result<(), BoxError> { - let root_span = if services.contains(&"client") { - trace.select_path("$..spans[?(@.operationName == 'client_request')]")?[0] - } else { - trace.select_path("$..spans[?(@.operationName == 'query ExampleQuery')]")?[0] - }; - let spans = trace.select_path("$..spans[*]")?; - for span in spans { - let mut span_path = vec![span.select_path("$.operationName")?[0] - .as_str() - .expect("operation name not not found")]; - let mut current = span; - while let Some(parent) = parent_span(trace, current) { - span_path.push( - parent.select_path("$.operationName")?[0] + fn verify_operation_name(&self, trace: &Value) -> Result<(), BoxError> { + if let Some(expected_operation_name) = &self.operation_name { + let binding = + trace.select_path("$..spans[?(@.operationName == 'supergraph')]..tags[?(@.key == 'graphql.operation.name')].value")?; + println!("binding: {:?}", binding); + let operation_name = binding.first(); + assert_eq!( + operation_name + .expect("graphql.operation.name expected") .as_str() - .expect("operation name not not found"), + .expect("graphql.operation.name must be a string"), + expected_operation_name ); - current = parent; } - tracing::debug!("span path to root: '{:?}'", span_path); - if current != root_span { - return Err(BoxError::from(format!( - "span {:?} did not have a path to the root span", - span.select_path("$.operationName")?, - ))); + Ok(()) + } + + fn verify_priority_sampled(&self, trace: &Value) -> Result<(), BoxError> { + if let Some(psr) = self.priority_sampled { + let binding = + trace.select_path("$..[?(@.service=='router')].metrics._sampling_priority_v1")?; + if binding.is_empty() { + return Err(BoxError::from("missing sampling priority")); + } + for sampling_priority in binding { + assert_eq!( + sampling_priority + .as_f64() + .expect("psr not string") + .to_string(), + psr + ); + } } + Ok(()) } - Ok(()) } -fn parent_span<'a>(trace: &'a Value, span: &'a Value) -> Option<&'a Value> { - span.select_path("$.references[?(@.refType == 'CHILD_OF')].spanID") - .ok()? - .into_iter() - .filter_map(|id| id.as_str()) - .filter_map(|id| { - trace - .select_path(&format!("$..spans[?(@.spanID == '{id}')]")) - .ok()? - .into_iter() - .next() - }) - .next() +impl TraceSpec { + async fn validate_jaeger_trace( + self, + router: &mut IntegrationTest, + query: Query, + ) -> Result<(), BoxError> { + JaegerTraceSpec { trace_spec: self } + .validate_trace(router, query) + .await + } } diff --git a/apollo-router/tests/integration/telemetry/logging.rs b/apollo-router/tests/integration/telemetry/logging.rs index 9e41160572..21c19a3246 100644 --- a/apollo-router/tests/integration/telemetry/logging.rs +++ b/apollo-router/tests/integration/telemetry/logging.rs @@ -1,8 +1,7 @@ -use serde_json::json; use tower::BoxError; use uuid::Uuid; -use crate::integration::common::graph_os_enabled; +use crate::integration::common::{graph_os_enabled, Query}; use crate::integration::common::IntegrationTest; use crate::integration::common::Telemetry; @@ -22,16 +21,15 @@ async fn test_json() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains("trace_id").await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains("span_id").await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains(r#""static_one":"test""#).await; #[cfg(unix)] { - router.execute_query(&query).await; + router.execute_default_query().await; router .assert_log_contains( r#""schema.id":"dd8960ccefda82ca58e8ac0bc266459fd49ee8215fd6b3cc72e7bc3d7f3464b9""#, @@ -39,11 +37,11 @@ async fn test_json() -> Result<(), BoxError> { .await; } - router.execute_query(&query).await; + router.execute_default_query().await; router .assert_log_contains(r#""on_supergraph_response_event":"on_supergraph_event""#) .await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains(r#""response_status":200"#).await; router.graceful_shutdown().await; @@ -66,24 +64,23 @@ async fn test_json_promote_span_attributes() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains("trace_id").await; - router.execute_query(&query).await; + router.execute_query(Query::default()).await; router.assert_log_contains("span_id").await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains(r#""static_one":"test""#).await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains(r#""response_status":200"#).await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains(r#""too_big":true"#).await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains(r#""too_big":"nope""#).await; - router.execute_query(&query).await; + router.execute_default_query().await; router .assert_log_contains(r#""graphql.document":"query ExampleQuery {topProducts{name}}""#) .await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_not_contains(r#""should_not_log""#).await; router.assert_log_not_contains(r#""another_one""#).await; router.graceful_shutdown().await; @@ -107,14 +104,13 @@ async fn test_json_uuid_format() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains("trace_id").await; - let (trace_id, _) = router.execute_query(&query).await; + let (trace_id, _) = router.execute_default_query().await; router .assert_log_contains(&format!("{}", Uuid::from_bytes(trace_id.to_bytes()))) .await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains("span_id").await; router.graceful_shutdown().await; @@ -137,14 +133,13 @@ async fn test_text_uuid_format() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains("trace_id").await; - let (trace_id, _) = router.execute_query(&query).await; + let (trace_id, _) = router.execute_default_query().await; router .assert_log_contains(&format!("{}", Uuid::from_bytes(trace_id.to_bytes()))) .await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains("span_id").await; router.graceful_shutdown().await; @@ -166,18 +161,17 @@ async fn test_json_sampler_off() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains("trace_id").await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains("span_id").await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains(r#""static_one":"test""#).await; - router.execute_query(&query).await; + router.execute_default_query().await; router .assert_log_contains(r#""on_supergraph_response_event":"on_supergraph_event""#) .await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains(r#""response_status":200"#).await; router.graceful_shutdown().await; @@ -200,17 +194,16 @@ async fn test_text() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - router.execute_query(&query).await; - router.execute_query(&query).await; + router.execute_query(Query::default()).await; + router.execute_query(Query::default()).await; router.assert_log_contains("trace_id").await; - router.execute_query(&query).await; + router.execute_query(Query::default()).await; router.assert_log_contains("span_id").await; router .assert_log_contains(r#"on_supergraph_response_event=on_supergraph_event"#) .await; - router.execute_query(&query).await; - router.execute_query(&query).await; + router.execute_query(Query::default()).await; + router.execute_query(Query::default()).await; router.assert_log_contains("response_status=200").await; router.graceful_shutdown().await; Ok(()) @@ -231,14 +224,12 @@ async fn test_text_sampler_off() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - router.execute_query(&query).await; - router.execute_query(&query).await; + router.execute_default_query().await; + router.execute_default_query().await; router.assert_log_contains("trace_id").await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains("span_id").await; - router.execute_query(&query).await; + router.execute_default_query().await; router.assert_log_contains("response_status=200").await; router.graceful_shutdown().await; Ok(()) diff --git a/apollo-router/tests/integration/telemetry/metrics.rs b/apollo-router/tests/integration/telemetry/metrics.rs index cbce509c13..37b1f1cae9 100644 --- a/apollo-router/tests/integration/telemetry/metrics.rs +++ b/apollo-router/tests/integration/telemetry/metrics.rs @@ -2,7 +2,7 @@ use std::time::Duration; use serde_json::json; -use crate::integration::common::graph_os_enabled; +use crate::integration::common::{graph_os_enabled, Query}; use crate::integration::IntegrationTest; const PROMETHEUS_CONFIG: &str = include_str!("fixtures/prometheus.router.yaml"); @@ -107,7 +107,7 @@ async fn test_subgraph_auth_metrics() { router.assert_reloaded().await; // This one will not be signed, counters shouldn't increment. router - .execute_query(&json! {{ "query": "query { me { name } }"}}) + .execute_query(Query::default()) .await; // Get Prometheus metrics. @@ -137,7 +137,9 @@ async fn test_metrics_bad_query() { router.start().await; router.assert_started().await; // This query won't make it to the supergraph service - router.execute_bad_query().await; + router + .execute_query(Query::default().with_bad_query()) + .await; router.assert_metrics_contains(r#"apollo_router_operations_total{http_response_status_code="400",otel_scope_name="apollo/router"} 1"#, None).await; } @@ -157,7 +159,9 @@ async fn test_bad_queries() { None, ) .await; - router.execute_bad_content_type().await; + router + .execute_query(Query::default().with_bad_content_type()) + .await; router .assert_metrics_contains( @@ -166,7 +170,9 @@ async fn test_bad_queries() { ) .await; - router.execute_bad_query().await; + router + .execute_query(Query::default().with_bad_query()) + .await; router .assert_metrics_contains( r#"apollo_router_http_requests_total{error="Must provide query string",status="400",otel_scope_name="apollo/router"}"#, @@ -174,7 +180,9 @@ async fn test_bad_queries() { ) .await; - router.execute_huge_query().await; + router + .execute_query(Query::default().with_huge_query()) + .await; router .assert_metrics_contains( r#"apollo_router_http_requests_total{error="Request body payload too large",status="413",otel_scope_name="apollo/router"} 1"#, @@ -260,13 +268,15 @@ async fn test_gauges_on_reload() { // Introspection query router - .execute_query(&json!({"query":"{__schema {types {name}}}","variables":{}})) + .execute_query(Query::introspection() + + ) .await; // Persisted query router .execute_query( - &json!({"query": "{__typename}", "variables":{}, "extensions": {"persistedQuery":{"version" : 1, "sha256Hash" : "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}}) + Query::builder().body(json!({"query": "{__typename}", "variables":{}, "extensions": {"persistedQuery":{"version" : 1, "sha256Hash" : "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}})).build() ) .await; diff --git a/apollo-router/tests/integration/telemetry/mod.rs b/apollo-router/tests/integration/telemetry/mod.rs index 8df0a1d753..f79382f772 100644 --- a/apollo-router/tests/integration/telemetry/mod.rs +++ b/apollo-router/tests/integration/telemetry/mod.rs @@ -1,3 +1,5 @@ +use std::collections::{HashMap, HashSet}; + #[cfg(any(not(feature = "ci"), all(target_arch = "x86_64", target_os = "linux")))] mod datadog; #[cfg(any(not(feature = "ci"), all(target_arch = "x86_64", target_os = "linux")))] @@ -8,3 +10,19 @@ mod otlp; mod propagation; #[cfg(any(not(feature = "ci"), all(target_arch = "x86_64", target_os = "linux")))] mod zipkin; +mod verifier; + +#[derive(buildstructor::Builder)] +struct TraceSpec { + operation_name: Option, + version: Option, + services: Vec<&'static str>, + span_names: HashSet<&'static str>, + measured_spans: HashSet<&'static str>, + unmeasured_spans: HashSet<&'static str>, + priority_sampled: Option<&'static str>, + subgraph_sampled: Option, + span_attributes: HashMap<&'static str, &'static str> +} + + diff --git a/apollo-router/tests/integration/telemetry/otlp.rs b/apollo-router/tests/integration/telemetry/otlp.rs index 59dc15a518..6947879116 100644 --- a/apollo-router/tests/integration/telemetry/otlp.rs +++ b/apollo-router/tests/integration/telemetry/otlp.rs @@ -1,18 +1,13 @@ extern crate core; -use std::collections::HashMap; use std::collections::HashSet; -use std::sync::Arc; -use std::sync::Mutex; -use std::time::Duration; +use std::ops::Deref; use anyhow::anyhow; -use opentelemetry_api::trace::SpanContext; use opentelemetry_api::trace::TraceId; use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceResponse; use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceResponse; use prost::Message; -use serde_json::json; use serde_json::Value; use tower::BoxError; use wiremock::matchers::method; @@ -21,9 +16,11 @@ use wiremock::Mock; use wiremock::MockServer; use wiremock::ResponseTemplate; -use crate::integration::common::graph_os_enabled; use crate::integration::common::Telemetry; +use crate::integration::common::{graph_os_enabled, Query}; use crate::integration::IntegrationTest; +use crate::integration::telemetry::TraceSpec; +use crate::integration::telemetry::verifier::Verifier; use crate::integration::ValueExt; #[tokio::test(flavor = "multi_thread")] @@ -45,15 +42,8 @@ async fn test_basic() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); for _ in 0..2 { - let (id, result) = router.execute_query(&query).await; - assert!(!result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .is_empty()); - Spec::builder() + TraceSpec::builder() .operation_name("ExampleQuery") .services(["client", "router", "subgraph"].into()) .span_names( @@ -72,12 +62,16 @@ async fn test_basic() -> Result<(), BoxError> { ) .subgraph_sampled(true) .build() - .validate_trace(id, &mock_server) + .validate_otlp_trace( + &mut router, + &mock_server, + Query::default(), + ) .await?; - Spec::builder() + TraceSpec::builder() .service("router") .build() - .validate_metrics(&mock_server) + .validate_otlp_metrics(&mock_server) .await?; router.touch_config().await; router.assert_reloaded().await; @@ -98,6 +92,7 @@ async fn test_otlp_request_with_datadog_propagator() -> Result<(), BoxError> { .telemetry(Telemetry::Otlp { endpoint: Some(format!("{}/v1/traces", mock_server.uri())), }) + .extra_propagator(Telemetry::Datadog) .config(&config) .build() .await; @@ -105,14 +100,16 @@ async fn test_otlp_request_with_datadog_propagator() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, _) = router.execute_query(&query).await; - Spec::builder() + TraceSpec::builder() .services(["client", "router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) .build() - .validate_trace(id, &mock_server) + .validate_otlp_trace( + &mut router, + &mock_server, + Query::default(), + ) .await?; router.graceful_shutdown().await; Ok(()) @@ -137,13 +134,15 @@ async fn test_otlp_request_with_datadog_propagator_no_agent() -> Result<(), BoxE router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, _) = router.execute_query(&query).await; - Spec::builder() + TraceSpec::builder() .services(["client", "router", "subgraph"].into()) .subgraph_sampled(true) .build() - .validate_trace(id, &mock_server) + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(true).build(), + ) .await?; router.graceful_shutdown().await; Ok(()) @@ -158,12 +157,11 @@ async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( let mock_server = mock_otlp_server().await; let config = include_str!("fixtures/otlp_datadog_request_with_zipkin_propagator.router.yaml") .replace("", &mock_server.uri()); - let context = Arc::new(Mutex::new(None)); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Otlp { endpoint: Some(format!("{}/v1/traces", mock_server.uri())), }) - .subgraph_context(context.clone()) + .extra_propagator(Telemetry::Datadog) .config(&config) .build() .await; @@ -171,108 +169,99 @@ async fn test_otlp_request_with_zipkin_trace_context_propagator_with_datadog( router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, _) = router.execute_query(&query).await; - - Spec::builder() - .subgraph_context(context.clone()) + TraceSpec::builder() .services(["client", "router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) .build() - .validate_trace(id, &mock_server) + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(true).build(), + ) .await?; // ---------------------- zipkin propagator with unsampled trace // Testing for an unsampled trace, so it should be sent to the otlp exporter with sampling priority set 0 // But it shouldn't send the trace to subgraph as the trace is originally not sampled, the main goal is to measure it at the DD agent level - let id = TraceId::from_hex("80f198ee56343ba864fe8b2a57d3eff7").unwrap(); - let headers: HashMap = [ - ( - "X-B3-TraceId".to_string(), - "80f198ee56343ba864fe8b2a57d3eff7".to_string(), - ), - ( - "X-B3-ParentSpanId".to_string(), - "05e3ac9a4f6e3b90".to_string(), - ), - ("X-B3-SpanId".to_string(), "e457b5a2e4d86bd1".to_string()), - ("X-B3-Sampled".to_string(), "0".to_string()), - ] - .into(); - - let (_id, _) = router.execute_untraced_query(&query, Some(headers)).await; - Spec::builder() - .subgraph_context(context.clone()) + TraceSpec::builder() .services(["router"].into()) .priority_sampled("0") - .subgraph_sampled(true) + .subgraph_sampled(false) .build() - .validate_trace(id, &mock_server) + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder() + .traced(false) + .header("X-B3-TraceId", "80f198ee56343ba864fe8b2a57d3eff7") + .header("X-B3-ParentSpanId", "05e3ac9a4f6e3b90") + .header("X-B3-SpanId", "e457b5a2e4d86bd1") + .header("X-B3-Sampled", "0") + .build(), + ) .await?; // ---------------------- trace context propagation // Testing for a trace containing the right tracestate with m and psr for DD and a sampled trace, so it should be sent to the otlp exporter with sampling priority set to 1 // And it should also send the trace to subgraph as the trace is sampled - let id = TraceId::from_hex("0af7651916cd43dd8448eb211c80319c").unwrap(); - let headers: HashMap = [ - ( - "traceparent".to_string(), - "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(), - ), - ("tracestate".to_string(), "m=1,psr=1".to_string()), - ] - .into(); - - let (_id, _) = router.execute_untraced_query(&query, Some(headers)).await; - Spec::builder() - .subgraph_context(context.clone()) - .services(["router", "subgraph"].into()) + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) .build() - .validate_trace(id, &mock_server) + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder() + .traced(true) + .header( + "traceparent", + "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", + ) + .header("tracestate", "m=1,psr=1") + .build(), + ) .await?; // ---------------------- // Testing for a trace containing the right tracestate with m and psr for DD and an unsampled trace, so it should be sent to the otlp exporter with sampling priority set to 0 // But it shouldn't send the trace to subgraph as the trace is originally not sampled, the main goal is to measure it at the DD agent level - let id = TraceId::from_hex("0af7651916cd43dd8448eb211c80319d").unwrap(); - let headers: HashMap = [ - ( - "traceparent".to_string(), - "00-0af7651916cd43dd8448eb211c80319d-b7ad6b7169203331-00".to_string(), - ), - ("tracestate".to_string(), "m=1,psr=0".to_string()), - ] - .into(); - - let (_id, _) = router.execute_untraced_query(&query, Some(headers)).await; - Spec::builder() - .subgraph_context(context.clone()) + TraceSpec::builder() .services(["router"].into()) .priority_sampled("0") + .subgraph_sampled(false) .build() - .validate_trace(id, &mock_server) + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder() + .traced(false) + .header( + "traceparent", + "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-02", + ) + .header("tracestate", "m=1,psr=0") + .build(), + ) .await?; // ---------------------- // Testing for a trace containing a tracestate m and psr with psr set to 1 for DD and an unsampled trace, so it should be sent to the otlp exporter with sampling priority set to 1 // It should not send the trace to the subgraph as we didn't use the datadog propagator and therefore the trace will remain unsampled. - let id = TraceId::from_hex("0af7651916cd43dd8448eb211c80319e").unwrap(); - let headers: HashMap = [ - ( - "traceparent".to_string(), - "00-0af7651916cd43dd8448eb211c80319e-b7ad6b7169203331-00".to_string(), - ), - ("tracestate".to_string(), "m=1,psr=1".to_string()), - ] - .into(); - - let (_id, _) = router.execute_untraced_query(&query, Some(headers)).await; - Spec::builder() - .subgraph_context(context.clone()) - .services(["router"].into()) + TraceSpec::builder() + .services(["router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) .build() - .validate_trace(id, &mock_server) + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder() + .traced(false) + .header( + "traceparent", + "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-03", + ) + .header("tracestate", "m=1,psr=1") + .build(), + ) .await?; // Be careful if you add the same kind of test crafting your own trace id, make sure to increment the previous trace id by 1 if not you'll receive all the previous spans tested with the same trace id before @@ -288,25 +277,26 @@ async fn test_untraced_request_no_sample_datadog_agent() -> Result<(), BoxError> let mock_server = mock_otlp_server().await; let config = include_str!("fixtures/otlp_datadog_agent_no_sample.router.yaml") .replace("", &mock_server.uri()); - let context = Arc::new(Mutex::new(None)); - let mut router = IntegrationTest::builder() - .subgraph_context(context.clone()) - .config(&config) - .build() - .await; + let mut router = IntegrationTest::builder().config(&config) + .telemetry(Telemetry::Otlp { + endpoint: Some(format!("{}/v1/traces", mock_server.uri())), + }) + .extra_propagator(Telemetry::Datadog) + .build().await; router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, _) = router.execute_untraced_query(&query, None).await; - Spec::builder() - .subgraph_context(context) + TraceSpec::builder() .services(["router"].into()) .priority_sampled("0") .subgraph_sampled(false) .build() - .validate_trace(id, &mock_server) + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(false).build(), + ) .await?; router.graceful_shutdown().await; Ok(()) @@ -320,25 +310,27 @@ async fn test_untraced_request_sample_datadog_agent() -> Result<(), BoxError> { let mock_server = mock_otlp_server().await; let config = include_str!("fixtures/otlp_datadog_agent_sample.router.yaml") .replace("", &mock_server.uri()); - let context = Arc::new(Mutex::new(None)); let mut router = IntegrationTest::builder() - .subgraph_context(context.clone()) .config(&config) - .build() - .await; + .telemetry(Telemetry::Otlp { + endpoint: Some(format!("{}/v1/traces", mock_server.uri())), + }) + .extra_propagator(Telemetry::Datadog) + .build().await; router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, _) = router.execute_untraced_query(&query, None).await; - Spec::builder() - .subgraph_context(context.clone()) - .services(["router"].into()) + TraceSpec::builder() + .services(["router", "subgraph"].into()) .priority_sampled("1") .subgraph_sampled(true) .build() - .validate_trace(id, &mock_server) + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(false).build(), + ) .await?; router.graceful_shutdown().await; Ok(()) @@ -350,14 +342,13 @@ async fn test_untraced_request_sample_datadog_agent_unsampled() -> Result<(), Bo panic!("Error: test skipped because GraphOS is not enabled"); } let mock_server = mock_otlp_server().await; - let context = Arc::new(Mutex::new(None)); let config = include_str!("fixtures/otlp_datadog_agent_sample_no_sample.router.yaml") .replace("", &mock_server.uri()); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Otlp { endpoint: Some(format!("{}/v1/traces", mock_server.uri())), }) - .subgraph_context(context.clone()) + .extra_propagator(Telemetry::Datadog) .config(&config) .build() .await; @@ -365,15 +356,16 @@ async fn test_untraced_request_sample_datadog_agent_unsampled() -> Result<(), Bo router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let (id, _) = router.execute_untraced_query(&query, None).await; - Spec::builder() + TraceSpec::builder() .services(["router"].into()) .priority_sampled("0") .subgraph_sampled(false) - .subgraph_context(context.clone()) .build() - .validate_trace(id, &mock_server) + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(false).build(), + ) .await?; router.graceful_shutdown().await; Ok(()) @@ -387,7 +379,6 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { let mock_server = mock_otlp_server().await; let config = include_str!("fixtures/otlp_datadog_propagation.router.yaml") .replace("", &mock_server.uri()); - let context = Arc::new(Mutex::new(None)); let mut router = IntegrationTest::builder() // We're using datadog propagation as this is what we are trying to test. .telemetry(Telemetry::Otlp { @@ -395,10 +386,6 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { }) .extra_propagator(Telemetry::Datadog) .config(config) - .responder(ResponseTemplate::new(200).set_body_json( - json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), - )) - .subgraph_context(context.clone()) .build() .await; @@ -406,67 +393,63 @@ async fn test_priority_sampling_propagated() -> Result<(), BoxError> { router.assert_started().await; // Parent based sampling. psr MUST be populated with the value that we pass in. - test_psr( - &mut router, - Some("-1"), - Spec::builder() - .subgraph_context(context.clone()) - .services(["client", "router"].into()) - .priority_sampled("-1") - .subgraph_sampled(false) - .build(), - &mock_server, - ) - .await?; - test_psr( - &mut router, - Some("0"), - Spec::builder() - .subgraph_context(context.clone()) - .services(["client", "router"].into()) - .priority_sampled("0") - .subgraph_sampled(false) - .build(), - &mock_server, - ) - .await?; - test_psr( - &mut router, - Some("1"), - Spec::builder() - .services(["client", "router", "subgraph"].into()) - .priority_sampled("1") - .subgraph_sampled(true) - .build(), - &mock_server, - ) - .await?; - test_psr( - &mut router, - Some("2"), - Spec::builder() - .subgraph_context(context.clone()) - .services(["client", "router", "subgraph"].into()) - .priority_sampled("2") - .subgraph_sampled(true) - .build(), - &mock_server, - ) - .await?; + TraceSpec::builder() + .services(["client", "router"].into()) + .priority_sampled("-1") + .subgraph_sampled(false) + .build() + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(true).psr("-1").build(), + ) + .await?; + TraceSpec::builder() + .services(["client", "router"].into()) + .priority_sampled("0") + .subgraph_sampled(false) + .build() + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(true).psr("0").build(), + ) + .await?; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(true).psr("1").build(), + ) + .await?; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("2") + .subgraph_sampled(true) + .build() + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(true).psr("2").build(), + ) + .await?; // No psr was passed in the router is free to set it. This will be 1 as we are going to sample here. - test_psr( - &mut router, - None, - Spec::builder() - .subgraph_context(context.clone()) - .services(["client", "router", "subgraph"].into()) - .priority_sampled("1") - .subgraph_sampled(true) - .build(), - &mock_server, - ) - .await?; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(true).build(), + ) + .await?; router.graceful_shutdown().await; @@ -481,17 +464,12 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { let mock_server = mock_otlp_server().await; let config = include_str!("fixtures/otlp_datadog_propagation_no_parent_sampler.router.yaml") .replace("", &mock_server.uri()); - let context = Arc::new(Mutex::new(None)); let mut router = IntegrationTest::builder() .telemetry(Telemetry::Otlp { endpoint: Some(format!("{}/v1/traces", mock_server.uri())), }) .extra_propagator(Telemetry::Datadog) .config(config) - .responder(ResponseTemplate::new(200).set_body_json( - json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), - )) - .subgraph_context(context.clone()) .build() .await; @@ -499,168 +477,138 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { router.assert_started().await; // The router will ignore the upstream PSR as parent based sampling is disabled. - test_psr( - &mut router, - Some("-1"), - Spec::builder() - .subgraph_context(context.clone()) - .services(["client", "router", "subgraph"].into()) - .priority_sampled("1") - .subgraph_sampled(true) - .build(), - &mock_server, - ) - .await?; - test_psr( - &mut router, - Some("0"), - Spec::builder() - .subgraph_context(context.clone()) - .services(["client", "router", "subgraph"].into()) - .priority_sampled("1") - .subgraph_sampled(true) - .build(), - &mock_server, - ) - .await?; - test_psr( - &mut router, - Some("1"), - Spec::builder() - .subgraph_context(context.clone()) - .services(["client", "router", "subgraph"].into()) - .priority_sampled("1") - .subgraph_sampled(true) - .build(), - &mock_server, - ) - .await?; - test_psr( - &mut router, - Some("2"), - Spec::builder() - .subgraph_context(context.clone()) - .services(["client", "router", "subgraph"].into()) - .priority_sampled("1") - .subgraph_sampled(true) - .build(), - &mock_server, - ) - .await?; - - test_psr( - &mut router, - None, - Spec::builder() - .subgraph_context(context.clone()) - .services(["client", "router", "subgraph"].into()) - .priority_sampled("1") - .subgraph_sampled(true) - .build(), - &mock_server, - ) - .await?; - - router.graceful_shutdown().await; - Ok(()) -} + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(true).psr("-1").build(), + ) + .await?; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(true).psr("0").build(), + ) + .await?; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(true).psr("1").build(), + ) + .await?; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(true).psr("2").build(), + ) + .await?; -async fn test_psr( - router: &mut IntegrationTest, - psr: Option<&str>, - trace_spec: Spec, - mock_server: &MockServer, -) -> Result<(), BoxError> { - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); - let headers = if let Some(psr) = psr { - vec![("x-datadog-sampling-priority".to_string(), psr.to_string())] - } else { - vec![] - }; - let (id, result) = router - .execute_query_with_headers(&query, headers.into_iter().collect()) - .await; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .priority_sampled("1") + .subgraph_sampled(true) + .build() + .validate_otlp_trace( + &mut router, + &mock_server, + Query::builder().traced(true).build(), + ) + .await?; - assert!(result.status().is_success()); + router.graceful_shutdown().await; - trace_spec.validate_trace(id, mock_server).await?; Ok(()) } -#[derive(buildstructor::Builder)] -struct Spec { - subgraph_context: Option>>>, - operation_name: Option, - version: Option, - services: HashSet<&'static str>, - span_names: HashSet<&'static str>, - measured_spans: HashSet<&'static str>, - unmeasured_spans: HashSet<&'static str>, - priority_sampled: Option<&'static str>, - subgraph_sampled: Option, +struct OtlpTraceSpec<'a> { + trace_spec: TraceSpec, + mock_server: &'a MockServer } +impl Deref for OtlpTraceSpec<'_> { + type Target = TraceSpec; -impl Spec { - #[allow(clippy::too_many_arguments)] - async fn validate_trace(&self, id: TraceId, mock_server: &MockServer) -> Result<(), BoxError> { - for _ in 0..10 { - if self.find_valid_trace(id, mock_server).await.is_ok() { - return Ok(()); - } - tokio::time::sleep(Duration::from_millis(100)).await; - } - self.find_valid_trace(id, mock_server).await?; - - if let Some(subgraph_context) = &self.subgraph_context { - let subgraph_context = subgraph_context.lock().expect("poisoned"); - let subgraph_span_context = subgraph_context.as_ref().expect("state").clone(); + fn deref(&self) -> &Self::Target { + &self.trace_spec + } +} - assert_eq!( - subgraph_span_context.trace_state().get("psr"), - self.priority_sampled - ); - if let Some(sampled) = self.subgraph_sampled { - assert_eq!(subgraph_span_context.is_sampled(), sampled); - } - } +impl Verifier for OtlpTraceSpec<'_> { + fn verify_span_attributes(&self, _span: &Value) -> Result<(), BoxError> { + // TODO Ok(()) } + fn spec(&self) -> &TraceSpec { + &self.trace_spec + } - async fn validate_metrics(&self, mock_server: &MockServer) -> Result<(), BoxError> { - for _ in 0..10 { - if self.find_valid_metrics(mock_server).await.is_ok() { - return Ok(()); - } - tokio::time::sleep(Duration::from_millis(100)).await; + fn measured_span(&self, trace: &Value, name: &str) -> Result { + let binding1 = trace.select_path(&format!( + "$..[?(@.meta.['otel.original_name'] == '{}')].metrics.['_dd.measured']", + name + ))?; + let binding2 = trace.select_path(&format!( + "$..[?(@.name == '{}')].metrics.['_dd.measured']", + name + ))?; + Ok(binding1 + .first() + .or(binding2.first()) + .and_then(|v| v.as_f64()) + .map(|v| v == 1.0) + .unwrap_or_default()) + } + + async fn find_valid_metrics(&self) -> Result<(), BoxError> { + let requests = self.mock_server + .received_requests() + .await + .expect("Could not get otlp requests"); + if let Some(metrics) = requests.iter().find(|r| r.url.path().ends_with("/metrics")) { + let metrics = opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest::decode(bytes::Bytes::copy_from_slice(&metrics.body))?; + let json_metrics = serde_json::to_value(metrics)?; + // For now just validate service name. + self.verify_services(&json_metrics)?; + + Ok(()) + } else { + Err(anyhow!("No metrics received").into()) } - self.find_valid_metrics(mock_server).await?; - Ok(()) } - #[allow(clippy::too_many_arguments)] - async fn find_valid_trace( - &self, - trace_id: TraceId, - mock_server: &MockServer, - ) -> Result<(), BoxError> { - // A valid trace has: - // * All three services - // * The correct spans - // * All spans are parented - // * Required attributes of 'router' span has been set - - let requests = mock_server.received_requests().await; - let trace= Value::Array(requests.unwrap_or_default().iter().filter(|r| r.url.path().ends_with("/traces")) - .filter_map(|r|{ + + async fn get_trace(&self, trace_id: TraceId) -> Result { + let requests = self.mock_server.received_requests().await; + let trace = Value::Array(requests.unwrap_or_default().iter().filter(|r| r.url.path().ends_with("/traces")) + .filter_map(|r| { match opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest::decode( bytes::Bytes::copy_from_slice(&r.body), ) { Ok(trace) => { match serde_json::to_value(trace) { Ok(trace) => { - Some(trace) } + Some(trace) + } Err(_) => { None } @@ -671,22 +619,12 @@ impl Spec { } } }).filter(|t| { - let datadog_trace_id = TraceId::from_u128(trace_id.to_datadog() as u128); let trace_found1 = !t.select_path(&format!("$..[?(@.traceId == '{}')]", trace_id)).unwrap_or_default().is_empty(); let trace_found2 = !t.select_path(&format!("$..[?(@.traceId == '{}')]", datadog_trace_id)).unwrap_or_default().is_empty(); trace_found1 | trace_found2 }).collect()); - - self.verify_services(&trace)?; - self.verify_spans_present(&trace)?; - self.verify_measured_spans(&trace)?; - self.verify_operation_name(&trace)?; - self.verify_priority_sampled(&trace)?; - self.verify_version(&trace)?; - self.verify_span_kinds(&trace)?; - - Ok(()) + Ok(trace) } fn verify_version(&self, trace: &Value) -> Result<(), BoxError> { @@ -704,50 +642,9 @@ impl Spec { Ok(()) } - fn verify_measured_spans(&self, trace: &Value) -> Result<(), BoxError> { - for expected in &self.measured_spans { - assert!( - self.measured_span(trace, expected)?, - "missing measured span {}", - expected - ); - } - for unexpected in &self.unmeasured_spans { - assert!( - !self.measured_span(trace, unexpected)?, - "unexpected measured span {}", - unexpected - ); - } - Ok(()) - } - fn measured_span(&self, trace: &Value, name: &str) -> Result { - let binding1 = trace.select_path(&format!( - "$..[?(@.meta.['otel.original_name'] == '{}')].metrics.['_dd.measured']", - name - ))?; - let binding2 = trace.select_path(&format!( - "$..[?(@.name == '{}')].metrics.['_dd.measured']", - name - ))?; - Ok(binding1 - .first() - .or(binding2.first()) - .and_then(|v| v.as_f64()) - .map(|v| v == 1.0) - .unwrap_or_default()) - } - - fn verify_span_kinds(&self, trace: &Value) -> Result<(), BoxError> { - // Validate that the span.kind has been propagated. We can just do this for a selection of spans. - self.validate_span_kind(trace, "router", "server")?; - self.validate_span_kind(trace, "supergraph", "internal")?; - self.validate_span_kind(trace, "http_request", "client")?; - Ok(()) - } - fn verify_services(&self, trace: &Value) -> Result<(), BoxError> { + fn verify_services(&self, trace: &Value) -> Result<(), axum::BoxError> { let actual_services: HashSet = trace .select_path("$..resource.attributes..[?(@.key == 'service.name')].value.stringValue")? .into_iter() @@ -774,7 +671,7 @@ impl Spec { .filter_map(|span_name| span_name.as_string()) .collect(); let mut span_names: HashSet<&str> = self.span_names.clone(); - if self.services.contains("client") { + if self.services.contains(&"client") { span_names.insert("client_request"); } tracing::debug!("found spans {:?}", operation_names); @@ -816,6 +713,7 @@ impl Spec { Ok(()) } + fn verify_operation_name(&self, trace: &Value) -> Result<(), BoxError> { if let Some(expected_operation_name) = &self.operation_name { let binding = @@ -855,22 +753,6 @@ impl Spec { Ok(()) } - async fn find_valid_metrics(&self, mock_server: &MockServer) -> Result<(), BoxError> { - let requests = mock_server - .received_requests() - .await - .expect("Could not get otlp requests"); - if let Some(metrics) = requests.iter().find(|r| r.url.path().ends_with("/metrics")) { - let metrics = opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest::decode(bytes::Bytes::copy_from_slice(&metrics.body))?; - let json_metrics = serde_json::to_value(metrics)?; - // For now just validate service name. - self.verify_services(&json_metrics)?; - - Ok(()) - } else { - Err(anyhow!("No metrics received").into()) - } - } } async fn mock_otlp_server() -> MockServer { @@ -905,3 +787,19 @@ impl DatadogId for TraceId { u64::from_be_bytes(bytes.try_into().unwrap()) } } + + +impl TraceSpec { + async fn validate_otlp_trace(self, router: &mut IntegrationTest, mock_server: &MockServer, query: Query) -> Result<(), BoxError>{ + OtlpTraceSpec { + trace_spec: self, + mock_server + }.validate_trace(router, query).await + } + async fn validate_otlp_metrics(self, mock_server: &MockServer) -> Result<(), BoxError>{ + OtlpTraceSpec { + trace_spec: self, + mock_server + }.validate_metrics().await + } +} \ No newline at end of file diff --git a/apollo-router/tests/integration/telemetry/propagation.rs b/apollo-router/tests/integration/telemetry/propagation.rs index e458f1986c..d1b8258c6f 100644 --- a/apollo-router/tests/integration/telemetry/propagation.rs +++ b/apollo-router/tests/integration/telemetry/propagation.rs @@ -1,9 +1,9 @@ use serde_json::json; use tower::BoxError; -use crate::integration::common::graph_os_enabled; use crate::integration::common::IntegrationTest; use crate::integration::common::Telemetry; +use crate::integration::common::{graph_os_enabled, Query}; #[tokio::test(flavor = "multi_thread")] async fn test_trace_id_via_header() -> Result<(), BoxError> { @@ -12,8 +12,7 @@ async fn test_trace_id_via_header() -> Result<(), BoxError> { return Ok(()); } async fn make_call(router: &mut IntegrationTest, trace_id: &str) { - let _ = router.execute_query_with_headers(&json!({"query":"query {topProducts{name, name, name, name, name, name, name, name, name, name}}","variables":{}}), - [("id_from_header".to_string(), trace_id.to_string())].into()).await; + let _ = router.execute_query(Query::builder().body(json!({"query":"query {topProducts{name, name, name, name, name, name, name, name, name, name}}","variables":{}})).header("id_from_header".to_string(), trace_id.to_string()).build()).await; } let mut router = IntegrationTest::builder() diff --git a/apollo-router/tests/integration/telemetry/verifier.rs b/apollo-router/tests/integration/telemetry/verifier.rs new file mode 100644 index 0000000000..bf4ffe4d70 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/verifier.rs @@ -0,0 +1,160 @@ +use crate::integration::common::Query; +use crate::integration::telemetry::TraceSpec; +use crate::integration::IntegrationTest; +use opentelemetry_api::trace::{SpanContext, TraceId}; +use serde_json::Value; +use std::time::Duration; +use anyhow::anyhow; +use tower::BoxError; + +pub trait Verifier { + fn spec(&self) -> &TraceSpec; + async fn validate_trace(&self, router: &mut IntegrationTest, query: Query) -> Result<(), BoxError> { + let (id, response) = router.execute_query(query).await; + for _ in 0..20 { + if self.find_valid_trace(id).await.is_ok() { + break; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + self.find_valid_trace(id).await?; + let subgraph_context = router.subgraph_context(); + assert!(response.status().is_success()); + self.validate_subgraph(subgraph_context)?; + Ok(()) + + } + + async fn validate_metrics(&self) -> Result<(), BoxError> { + for _ in 0..10 { + if self.find_valid_metrics().await.is_ok() { + break; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + self.find_valid_metrics().await?; + Ok(()) + } + + async fn find_valid_metrics(&self) -> Result<(), BoxError> { + unimplemented!("find_valid_metrics") + } + + fn validate_subgraph( + &self, + subgraph_context: SpanContext, + ) -> Result<(), BoxError> { + self.validate_subgraph_priority_sampled(&subgraph_context)?; + self.validate_subgraph_sampled(&subgraph_context)?; + Ok(()) + } + fn validate_subgraph_sampled( + &self, + subgraph_context: &SpanContext, + ) -> Result<(), BoxError> { + if let Some(sampled) = self.spec().priority_sampled { + assert_eq!( + subgraph_context.trace_state().get("psr"), + Some(sampled), + "subgraph psr" + ); + } + + + Ok(()) + } + + fn validate_subgraph_priority_sampled( + &self, + subgraph_context: &SpanContext, + ) -> Result<(), BoxError>{ + if let Some(sampled) = self.spec().subgraph_sampled { + assert_eq!(subgraph_context.is_sampled(), sampled, "subgraph sampled"); + } + Ok(()) + } + + + + #[allow(clippy::too_many_arguments)] + async fn find_valid_trace(&self, trace_id: TraceId) -> Result<(), BoxError> { + // A valid trace has: + // * All three services + // * The correct spans + // * All spans are parented + // * Required attributes of 'router' span has been set + + // For now just validate service name. + let trace: Value = self.get_trace(trace_id).await?; + println!("trace: {}", trace_id); + self.verify_services(&trace)?; + println!("services verified"); + self.verify_spans_present(&trace)?; + println!("spans present verified"); + self.verify_measured_spans(&trace)?; + println!("measured spans verified"); + self.verify_operation_name(&trace)?; + println!("operation name verified"); + self.verify_priority_sampled(&trace)?; + println!("priority sampled verified"); + self.verify_version(&trace)?; + println!("version verified"); + self.verify_span_kinds(&trace)?; + println!("span kinds verified"); + self.verify_span_attributes(&trace)?; + println!("span attributes verified"); + Ok(()) + } + + async fn get_trace(&self, trace_id: TraceId) -> Result; + + fn verify_version(&self, trace: &Value) -> Result<(), BoxError>; + + fn verify_measured_spans(&self, trace: &Value) -> Result<(), BoxError> { + for expected in &self.spec().measured_spans { + let measured = self.measured_span(trace, expected)?; + if !measured { + return Err(anyhow!("missing measured span {}", expected).into()); + } + } + for unexpected in &self.spec().unmeasured_spans { + let measured = self.measured_span(trace, unexpected)?; + if measured { + return Err(anyhow!("unexpected measured span {}", measured).into()); + } + } + Ok(()) + } + + fn measured_span(&self, trace: &Value, name: &str) -> Result; + + fn verify_span_kinds(&self, trace: &Value) -> Result<(), BoxError> { + // Validate that the span.kind has been propagated. We can just do this for a selection of spans. + if self.spec().span_names.contains("router") { + self.validate_span_kind(trace, "router", "server")?; + } + + if self.spec().span_names.contains("supergraph") { + self.validate_span_kind(trace, "supergraph", "internal")?; + } + + if self.spec().span_names.contains("http_request") { + self.validate_span_kind(trace, "http_request", "client")?; + } + + Ok(()) + } + + fn verify_services(&self, trace: &Value) -> Result<(), BoxError>; + + fn verify_spans_present(&self, trace: &Value) -> Result<(), BoxError>; + + fn validate_span_kind(&self, trace: &Value, name: &str, kind: &str) -> Result<(), BoxError>; + + fn verify_span_attributes(&self, trace: &Value) -> Result<(), BoxError>; + + fn verify_operation_name(&self, trace: &Value) -> Result<(), BoxError>; + + fn verify_priority_sampled(&self, trace: &Value) -> Result<(), BoxError>; + +} diff --git a/apollo-router/tests/integration/telemetry/zipkin.rs b/apollo-router/tests/integration/telemetry/zipkin.rs index c0d5e0a8d5..460c10add4 100644 --- a/apollo-router/tests/integration/telemetry/zipkin.rs +++ b/apollo-router/tests/integration/telemetry/zipkin.rs @@ -1,18 +1,18 @@ extern crate core; use std::collections::HashSet; -use std::time::Duration; +use std::ops::Deref; +use crate::integration::common::{Query, Telemetry}; +use crate::integration::telemetry::verifier::Verifier; +use crate::integration::telemetry::TraceSpec; +use crate::integration::IntegrationTest; +use crate::integration::ValueExt; use anyhow::anyhow; use opentelemetry_api::trace::TraceId; -use serde_json::json; use serde_json::Value; use tower::BoxError; -use crate::integration::common::Telemetry; -use crate::integration::IntegrationTest; -use crate::integration::ValueExt; - #[tokio::test(flavor = "multi_thread")] async fn test_basic() -> Result<(), BoxError> { let mut router = IntegrationTest::builder() @@ -24,22 +24,13 @@ async fn test_basic() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); for _ in 0..2 { - let (id, result) = router.execute_query(&query).await; - assert!(!result - .headers() - .get("apollo-custom-trace-id") - .unwrap() - .is_empty()); - validate_trace( - id, - &query, - Some("ExampleQuery"), - &["client", "router", "subgraph"], - false, - ) - .await?; + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .operation_name("ExampleQuery") + .build() + .validate_zipkin_trace(&mut router, Query::default()) + .await?; router.touch_config().await; router.assert_reloaded().await; } @@ -47,85 +38,115 @@ async fn test_basic() -> Result<(), BoxError> { Ok(()) } -async fn validate_trace( - id: TraceId, - query: &Value, - operation_name: Option<&str>, - services: &[&'static str], - custom_span_instrumentation: bool, -) -> Result<(), BoxError> { - let params = url::form_urlencoded::Serializer::new(String::new()) - .append_pair("service", services.first().expect("expected root service")) - .finish(); - - let url = format!("http://localhost:9411/api/v2/trace/{id}?{params}"); - for _ in 0..10 { - if find_valid_trace( - &url, - query, - operation_name, - services, - custom_span_instrumentation, - ) - .await - .is_ok() - { - return Ok(()); - } - tokio::time::sleep(Duration::from_millis(100)).await; + +struct ZipkinTraceSpec { + trace_spec: TraceSpec, +} +impl Deref for ZipkinTraceSpec { + type Target = TraceSpec; + + fn deref(&self) -> &Self::Target { + &self.trace_spec } - find_valid_trace( - &url, - query, - operation_name, - services, - custom_span_instrumentation, - ) - .await?; - Ok(()) } -async fn find_valid_trace( - url: &str, - _query: &Value, - _operation_name: Option<&str>, - services: &[&'static str], - _custom_span_instrumentation: bool, -) -> Result<(), BoxError> { - // A valid trace has: - // * All three services - // * The correct spans - // * All spans are parented - // * Required attributes of 'router' span has been set - - // For now just validate service name. - let trace: Value = reqwest::get(url) - .await - .map_err(|e| anyhow!("failed to contact zipkin; {}", e))? - .json() - .await?; - tracing::debug!("{}", serde_json::to_string_pretty(&trace)?); - verify_trace_participants(&trace, services)?; +impl Verifier for ZipkinTraceSpec { + fn verify_span_attributes(&self, _trace: &Value) -> Result<(), BoxError> { + Ok(()) + } + fn verify_version(&self, _trace: &Value) -> Result<(), BoxError> { - Ok(()) + Ok(()) + } + + + fn measured_span(&self, _trace: &Value, _name: &str) -> Result { + Ok(true) + } + + fn verify_span_kinds(&self, _trace: &Value) -> Result<(), BoxError> { + Ok(()) + } + + fn verify_services(&self, trace: &Value) -> Result<(), axum::BoxError> { + let actual_services: HashSet = trace + .select_path("$..serviceName")? + .into_iter() + .filter_map(|service| service.as_string()) + .collect(); + tracing::debug!("found services {:?}", actual_services); + + let expected_services = self.trace_spec.services + .iter() + .map(|s| s.to_string()) + .collect::>(); + if actual_services != expected_services { + return Err(BoxError::from(format!( + "incomplete traces, got {actual_services:?} expected {expected_services:?}" + ))); + } + Ok(()) + } + + fn verify_spans_present(&self, _trace: &Value) -> Result<(), BoxError> { + Ok(()) + } + + fn validate_span_kind(&self, _trace: &Value, _name: &str, _kind: &str) -> Result<(), BoxError> { + Ok(()) + } + + fn verify_operation_name(&self, trace: &Value) -> Result<(), BoxError> { + if let Some(expected_operation_name) = &self.operation_name { + let binding = + trace.select_path("$..[?(@.name == 'supergraph')].tags..['graphql.operation.name']")?; + let operation_name = binding.first(); + assert_eq!( + operation_name + .expect("graphql.operation.name expected") + .as_str() + .expect("graphql.operation.name must be a string"), + expected_operation_name + ); + } + Ok(()) + } + + fn verify_priority_sampled(&self, _trace: &Value) -> Result<(), BoxError> { + Ok(()) + } + + + async fn get_trace(&self, trace_id: TraceId) -> Result { + let params = url::form_urlencoded::Serializer::new(String::new()) + .append_pair("service", self.trace_spec.services.first().expect("expected root service")) + .finish(); + + let id = trace_id.to_string(); + let url = format!("http://localhost:9411/api/v2/trace/{id}?{params}"); + println!("url: {}", url); + let value: serde_json::Value = reqwest::get(url) + .await + .map_err(|e| anyhow!("failed to contact datadog; {}", e))? + .json() + .await + .map_err(|e| anyhow!("failed to contact datadog; {}", e))?; + Ok(value) + } + + fn spec(&self) -> &TraceSpec { + &self.trace_spec + } } -fn verify_trace_participants(trace: &Value, services: &[&'static str]) -> Result<(), BoxError> { - let actual_services: HashSet = trace - .select_path("$..serviceName")? - .into_iter() - .filter_map(|service| service.as_string()) - .collect(); - tracing::debug!("found services {:?}", actual_services); - - let expected_services = services - .iter() - .map(|s| s.to_string()) - .collect::>(); - if actual_services != expected_services { - return Err(BoxError::from(format!( - "incomplete traces, got {actual_services:?} expected {expected_services:?}" - ))); +impl TraceSpec { + async fn validate_zipkin_trace( + self, + router: &mut IntegrationTest, + query: Query, + ) -> Result<(), BoxError> { + ZipkinTraceSpec { trace_spec: self } + .validate_trace(router, query) + .await } - Ok(()) } diff --git a/apollo-router/tests/integration/traffic_shaping.rs b/apollo-router/tests/integration/traffic_shaping.rs index feb9a7e725..5d8b45e28b 100644 --- a/apollo-router/tests/integration/traffic_shaping.rs +++ b/apollo-router/tests/integration/traffic_shaping.rs @@ -5,7 +5,7 @@ use serde_json::json; use tower::BoxError; use wiremock::ResponseTemplate; -use crate::integration::common::graph_os_enabled; +use crate::integration::common::{graph_os_enabled, Query}; use crate::integration::common::Telemetry; use crate::integration::IntegrationTest; @@ -99,9 +99,9 @@ async fn test_router_timeout_operation_name_in_tracing() -> Result<(), BoxError> router.assert_started().await; let (_trace_id, response) = router - .execute_query(&json!({ + .execute_query(Query::builder().body(json!({ "query": "query UniqueName { topProducts { name } }" - })) + })).build()) .await; assert_eq!(response.status(), 504); let response = response.text().await?; diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index 7f06f1d5cc..abc45e2ff1 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -30,6 +30,7 @@ use wiremock::ResponseTemplate; #[path = "./common.rs"] pub(crate) mod common; pub(crate) use common::IntegrationTest; +use crate::common::Query; fn main() -> Result> { let args = Arguments::from_args(); @@ -497,7 +498,7 @@ impl TestExecution { writeln!(out, "header: {:?}\n", headers).unwrap(); let (_, response) = router - .execute_query_with_headers(&request, headers.clone()) + .execute_query(Query::builder().body(request).headers(headers.clone()).build()) .await; writeln!(out, "response headers: {:?}", response.headers()).unwrap(); From 530eecd5c802d2ab4d5a2622c5559146cdfc96b9 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 13 Dec 2024 11:04:28 +0000 Subject: [PATCH 23/35] Fix coprocessor test --- apollo-router/tests/integration/coprocessor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-router/tests/integration/coprocessor.rs b/apollo-router/tests/integration/coprocessor.rs index 21c5f0db2b..dd7adcfbbf 100644 --- a/apollo-router/tests/integration/coprocessor.rs +++ b/apollo-router/tests/integration/coprocessor.rs @@ -43,7 +43,7 @@ async fn test_coprocessor_limit_payload() -> Result<(), BoxError> { // Expect a small query Mock::given(method("POST")) .and(path("/")) - .and(body_partial_json(json!({"version":1,"stage":"RouterRequest","control":"continue","body":"{\"query\":\"query {topProducts{name}}\",\"variables\":{}}","method":"POST"}))) + .and(body_partial_json(json!({"version":1,"stage":"RouterRequest","control":"continue","body":"{\"query\":\"query ExampleQuery {topProducts{name}}\",\"variables\":{}}","method":"POST"}))) .respond_with( ResponseTemplate::new(200).set_body_json(json!({"version":1,"stage":"RouterRequest","control":"continue","body":"{\"query\":\"query {topProducts{name}}\",\"variables\":{}}","method":"POST"})), ) From 0a1e1f7cfece3d51e4647e6b6592b7d86d54d2c9 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 13 Dec 2024 11:07:56 +0000 Subject: [PATCH 24/35] Fix redis test --- apollo-router/tests/common.rs | 5 +++++ apollo-router/tests/integration/redis.rs | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index 5374059ccf..6e527834e1 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -116,6 +116,11 @@ impl Query { self } + pub fn with_anonymous(mut self) -> Self { + self.body = json!({"query":"query {topProducts{name}}","variables":{}}); + self + } + pub fn with_huge_query(mut self) -> Self { self.body = json!({"query":"query {topProducts{name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name}}","variables":{}}); self diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index f5e3da3438..a19d2fd261 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -41,7 +41,7 @@ use serde_json::Value; use tower::BoxError; use tower::ServiceExt; -use crate::integration::common::graph_os_enabled; +use crate::integration::common::{graph_os_enabled, Query}; use crate::integration::IntegrationTest; #[tokio::test(flavor = "multi_thread")] @@ -1109,11 +1109,11 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key ); assert_ne!(starting_key, new_cache_key, "starting_key (cache key for the initial config) and new_cache_key (cache key with the updated config) should not be equal. This either means that the cache key is not being generated correctly, or that the test is not actually checking the updated key."); - router.execute_default_query().await; + router.execute_query(Query::default().with_anonymous()).await; router.assert_redis_cache_contains(starting_key, None).await; router.update_config(updated_config).await; router.assert_reloaded().await; - router.execute_default_query().await; + router.execute_query(Query::default().with_anonymous()).await; router .assert_redis_cache_contains(new_cache_key, Some(starting_key)) .await; From 14c3ed6e81900dbfd265f28bd92f8aaa146b45fe Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 13 Dec 2024 12:02:01 +0000 Subject: [PATCH 25/35] Lint --- apollo-router/tests/common.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index 4d53fddf19..7dd7636a00 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -106,26 +106,31 @@ impl Query { } } impl Query { + #[allow(dead_code)] pub fn with_bad_content_type(mut self) -> Self { self.content_type = "garbage".to_string(); self } + #[allow(dead_code)] pub fn with_bad_query(mut self) -> Self { self.body = json!({"garbage":{}}); self } + #[allow(dead_code)] pub fn with_anonymous(mut self) -> Self { self.body = json!({"query":"query {topProducts{name}}","variables":{}}); self } + #[allow(dead_code)] pub fn with_huge_query(mut self) -> Self { self.body = json!({"query":"query {topProducts{name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name}}","variables":{}}); self } + #[allow(dead_code)] pub fn introspection() -> Query { Query::builder() .body(json!({"query":"{__schema {types {name}}}","variables":{}})) @@ -492,6 +497,7 @@ impl IntegrationTest { Dispatch::new(subscriber) } + #[allow(dead_code)] pub fn subgraph_context(&self) -> SpanContext { self.subgraph_context .lock() @@ -627,12 +633,14 @@ impl IntegrationTest { fs::copy(supergraph_path, &self.test_schema_location).expect("could not write schema"); } + #[allow(dead_code)] pub fn execute_default_query( &self, ) -> impl std::future::Future { self.execute_query(Query::builder().build()) } + #[allow(dead_code)] pub fn execute_query( &self, query: Query, From 1c0a232e6c0295d8b141ee7d5bb65991180ea43c Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 13 Dec 2024 13:50:54 +0000 Subject: [PATCH 26/35] Lint --- apollo-router/tests/common.rs | 11 ++- apollo-router/tests/integration/batching.rs | 7 +- .../tests/integration/coprocessor.rs | 7 +- .../tests/integration/introspection.rs | 5 +- .../tests/integration/operation_limits.rs | 9 ++- .../query_planner/max_evaluated_plans.rs | 25 +++++-- apollo-router/tests/integration/redis.rs | 11 ++- .../tests/integration/subgraph_response.rs | 69 ++++++++++++----- .../tests/integration/subscription.rs | 8 +- apollo-router/tests/integration/supergraph.rs | 16 +++- .../tests/integration/telemetry/datadog.rs | 1 - .../tests/integration/telemetry/jaeger.rs | 75 +++++++++++++------ .../tests/integration/telemetry/logging.rs | 3 +- .../tests/integration/telemetry/metrics.rs | 13 +--- .../tests/integration/telemetry/mod.rs | 37 +++++++-- .../tests/integration/telemetry/otlp.rs | 63 ++++++++-------- .../integration/telemetry/propagation.rs | 3 +- .../tests/integration/telemetry/verifier.rs | 36 ++++----- .../tests/integration/telemetry/zipkin.rs | 32 ++++---- .../tests/integration/traffic_shaping.rs | 13 +++- apollo-router/tests/samples_tests.rs | 8 +- 21 files changed, 294 insertions(+), 158 deletions(-) diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index 7dd7636a00..e70cf5f0de 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; -use buildstructor::{buildstructor}; +use buildstructor::buildstructor; use fred::clients::RedisClient; use fred::interfaces::ClientLike; use fred::interfaces::KeysInterface; @@ -335,7 +335,11 @@ impl Telemetry { } } - pub(crate) fn extract_context(&self, request: &wiremock::Request, context: &Context) -> Context { + pub(crate) fn extract_context( + &self, + request: &wiremock::Request, + context: &Context, + ) -> Context { let headers: HashMap = request .headers .iter() @@ -362,8 +366,7 @@ impl Telemetry { .expect("psr"); let new_trace_id = if original_span_context.is_valid() { original_span_context.trace_id() - } - else { + } else { context.span().span_context().trace_id() }; context = context.with_remote_span_context(SpanContext::new( diff --git a/apollo-router/tests/integration/batching.rs b/apollo-router/tests/integration/batching.rs index f9e7ba6ab8..521e615b30 100644 --- a/apollo-router/tests/integration/batching.rs +++ b/apollo-router/tests/integration/batching.rs @@ -856,7 +856,8 @@ mod helper { use wiremock::ResponseTemplate; use super::test_is_enabled; - use crate::integration::common::{IntegrationTest, Query}; + use crate::integration::common::IntegrationTest; + use crate::integration::common::Query; /// Helper type for specifying a valid handler pub type Handler = fn(&wiremock::Request) -> ResponseTemplate; @@ -916,7 +917,9 @@ mod helper { // Execute the request let request = serde_json::to_value(requests)?; - let (_span, response) = router.execute_query(Query::builder().body(request).build()).await; + let (_span, response) = router + .execute_query(Query::builder().body(request).build()) + .await; serde_json::from_slice::>(&response.bytes().await?).map_err(BoxError::from) } diff --git a/apollo-router/tests/integration/coprocessor.rs b/apollo-router/tests/integration/coprocessor.rs index dd7adcfbbf..d82d15ca7c 100644 --- a/apollo-router/tests/integration/coprocessor.rs +++ b/apollo-router/tests/integration/coprocessor.rs @@ -7,7 +7,8 @@ use wiremock::matchers::path; use wiremock::Mock; use wiremock::ResponseTemplate; -use crate::integration::common::{graph_os_enabled, Query}; +use crate::integration::common::graph_os_enabled; +use crate::integration::common::Query; use crate::integration::IntegrationTest; #[tokio::test(flavor = "multi_thread")] @@ -75,7 +76,9 @@ async fn test_coprocessor_limit_payload() -> Result<(), BoxError> { assert_eq!(response.status(), 200); // This query is huge and will be rejected because it is too large before hitting the coprocessor - let (_trace_id, response) = router.execute_query(Query::default().with_huge_query()).await; + let (_trace_id, response) = router + .execute_query(Query::default().with_huge_query()) + .await; assert_eq!(response.status(), 413); assert_yaml_snapshot!(response.text().await?); diff --git a/apollo-router/tests/integration/introspection.rs b/apollo-router/tests/integration/introspection.rs index 56b2a496cd..8ad142a9cb 100644 --- a/apollo-router/tests/integration/introspection.rs +++ b/apollo-router/tests/integration/introspection.rs @@ -1,10 +1,11 @@ -use crate::integration::common::Query; -use crate::integration::IntegrationTest; use apollo_router::plugin::test::MockSubgraph; use apollo_router::services::supergraph::Request; use serde_json::json; use tower::ServiceExt; +use crate::integration::common::Query; +use crate::integration::IntegrationTest; + #[tokio::test] async fn simple() { let request = Request::fake_builder() diff --git a/apollo-router/tests/integration/operation_limits.rs b/apollo-router/tests/integration/operation_limits.rs index b0c5b25802..1b6b186e41 100644 --- a/apollo-router/tests/integration/operation_limits.rs +++ b/apollo-router/tests/integration/operation_limits.rs @@ -9,6 +9,7 @@ use apollo_router::TestHarness; use serde_json::json; use tower::BoxError; use tower::ServiceExt; + use crate::integration::common::Query; use crate::integration::IntegrationTest; @@ -310,7 +311,9 @@ async fn test_request_bytes_limit_with_coprocessor() -> Result<(), BoxError> { .await; router.start().await; router.assert_started().await; - let (_, resp) = router.execute_query(Query::default().with_huge_query()).await; + let (_, resp) = router + .execute_query(Query::default().with_huge_query()) + .await; assert_eq!(resp.status(), 413); router.graceful_shutdown().await; Ok(()) @@ -324,7 +327,9 @@ async fn test_request_bytes_limit() -> Result<(), BoxError> { .await; router.start().await; router.assert_started().await; - let (_, resp) = router.execute_query(Query::default().with_huge_query()).await; + let (_, resp) = router + .execute_query(Query::default().with_huge_query()) + .await; assert_eq!(resp.status(), 413); router.graceful_shutdown().await; Ok(()) diff --git a/apollo-router/tests/integration/query_planner/max_evaluated_plans.rs b/apollo-router/tests/integration/query_planner/max_evaluated_plans.rs index f3edb84232..6326d600ee 100644 --- a/apollo-router/tests/integration/query_planner/max_evaluated_plans.rs +++ b/apollo-router/tests/integration/query_planner/max_evaluated_plans.rs @@ -1,4 +1,5 @@ use serde_json::json; + use crate::integration::common::Query; use crate::integration::IntegrationTest; @@ -31,10 +32,14 @@ async fn reports_evaluated_plans() { router.start().await; router.assert_started().await; router - .execute_query(Query::builder().body(json!({ - "query": r#"{ t { v1 v2 v3 v4 } }"#, - "variables": {}, - })).build()) + .execute_query( + Query::builder() + .body(json!({ + "query": r#"{ t { v1 v2 v3 v4 } }"#, + "variables": {}, + })) + .build(), + ) .await; let metrics = router @@ -70,10 +75,14 @@ async fn does_not_exceed_max_evaluated_plans() { router.start().await; router.assert_started().await; router - .execute_query(Query::builder().body(json!({ - "query": r#"{ t { v1 v2 v3 v4 } }"#, - "variables": {}, - })).build()) + .execute_query( + Query::builder() + .body(json!({ + "query": r#"{ t { v1 v2 v3 v4 } }"#, + "variables": {}, + })) + .build(), + ) .await; let metrics = router diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index ea9f37be62..07d16d7b92 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -40,7 +40,8 @@ use serde_json::Value; use tower::BoxError; use tower::ServiceExt; -use crate::integration::common::{graph_os_enabled, Query}; +use crate::integration::common::graph_os_enabled; +use crate::integration::common::Query; use crate::integration::IntegrationTest; #[tokio::test(flavor = "multi_thread")] @@ -1072,11 +1073,15 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key ); assert_ne!(starting_key, new_cache_key, "starting_key (cache key for the initial config) and new_cache_key (cache key with the updated config) should not be equal. This either means that the cache key is not being generated correctly, or that the test is not actually checking the updated key."); - router.execute_query(Query::default().with_anonymous()).await; + router + .execute_query(Query::default().with_anonymous()) + .await; router.assert_redis_cache_contains(starting_key, None).await; router.update_config(updated_config).await; router.assert_reloaded().await; - router.execute_query(Query::default().with_anonymous()).await; + router + .execute_query(Query::default().with_anonymous()) + .await; router .assert_redis_cache_contains(new_cache_key, Some(starting_key)) .await; diff --git a/apollo-router/tests/integration/subgraph_response.rs b/apollo-router/tests/integration/subgraph_response.rs index cba16ca370..3f0f194d92 100644 --- a/apollo-router/tests/integration/subgraph_response.rs +++ b/apollo-router/tests/integration/subgraph_response.rs @@ -1,6 +1,7 @@ use serde_json::json; use tower::BoxError; use wiremock::ResponseTemplate; + use crate::integration::common::Query; use crate::integration::IntegrationTest; @@ -21,7 +22,9 @@ async fn test_subgraph_returning_data_null() -> Result<(), BoxError> { router.assert_started().await; let query = "{ __typename topProducts { name } }"; - let (_trace_id, response) = router.execute_query(Query::builder().body(json!({ "query": query })).build()).await; + let (_trace_id, response) = router + .execute_query(Query::builder().body(json!({ "query": query })).build()) + .await; assert_eq!(response.status(), 200); assert_eq!( response.json::().await?, @@ -64,7 +67,9 @@ async fn test_subgraph_returning_different_typename_on_query_root() -> Result<() inside_fragment: __typename } "#; - let (_trace_id, response) = router.execute_query(Query::builder().body(json!({ "query": query })).build()).await; + let (_trace_id, response) = router + .execute_query(Query::builder().body(json!({ "query": query })).build()) + .await; assert_eq!(response.status(), 200); assert_eq!( response.json::().await?, @@ -99,7 +104,11 @@ async fn test_valid_extensions_service_for_subgraph_error() -> Result<(), BoxErr router.assert_started().await; let (_trace_id, response) = router - .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) + .execute_query( + Query::builder() + .body(json!({ "query": "{ topProducts { name } }" })) + .build(), + ) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -141,7 +150,11 @@ async fn test_valid_extensions_service_is_preserved_for_subgraph_error() -> Resu router.assert_started().await; let (_trace_id, response) = router - .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) + .execute_query( + Query::builder() + .body(json!({ "query": "{ topProducts { name } }" })) + .build(), + ) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -174,7 +187,11 @@ async fn test_valid_extensions_service_for_invalid_subgraph_response() -> Result router.assert_started().await; let (_trace_id, response) = router - .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) + .execute_query( + Query::builder() + .body(json!({ "query": "{ topProducts { name } }" })) + .build(), + ) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -222,7 +239,11 @@ async fn test_valid_error_locations() -> Result<(), BoxError> { router.assert_started().await; let (_trace_id, response) = router - .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) + .execute_query( + Query::builder() + .body(json!({ "query": "{ topProducts { name } }" })) + .build(), + ) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -264,7 +285,11 @@ async fn test_empty_error_locations() -> Result<(), BoxError> { router.assert_started().await; let (_trace_id, response) = router - .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) + .execute_query( + Query::builder() + .body(json!({ "query": "{ topProducts { name } }" })) + .build(), + ) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -302,7 +327,11 @@ async fn test_invalid_error_locations() -> Result<(), BoxError> { router.assert_started().await; let (_trace_id, response) = router - .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) + .execute_query( + Query::builder() + .body(json!({ "query": "{ topProducts { name } }" })) + .build(), + ) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -345,7 +374,11 @@ async fn test_invalid_error_locations_with_single_negative_one_location() -> Res router.assert_started().await; let (_trace_id, response) = router - .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) + .execute_query( + Query::builder() + .body(json!({ "query": "{ topProducts { name } }" })) + .build(), + ) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -387,7 +420,11 @@ async fn test_invalid_error_locations_contains_negative_one_location() -> Result router.assert_started().await; let (_trace_id, response) = router - .execute_query(Query::builder().body(json!({ "query": "{ topProducts { name } }" })).build()) + .execute_query( + Query::builder() + .body(json!({ "query": "{ topProducts { name } }" })) + .build(), + ) .await; assert_eq!(response.status(), 200); assert_eq!( @@ -427,9 +464,7 @@ async fn test_valid_error_path() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let (_trace_id, response) = router - .execute_query(Query::default()) - .await; + let (_trace_id, response) = router.execute_query(Query::default()).await; assert_eq!(response.status(), 200); assert_eq!( response.json::().await?, @@ -464,9 +499,7 @@ async fn test_invalid_error_path() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let (_trace_id, response) = router - .execute_query(Query::default()) - .await; + let (_trace_id, response) = router.execute_query(Query::default()).await; assert_eq!(response.status(), 200); assert_eq!( response.json::().await?, @@ -502,9 +535,7 @@ async fn test_partially_valid_error_path() -> Result<(), BoxError> { router.start().await; router.assert_started().await; - let (_trace_id, response) = router - .execute_query(Query::default()) - .await; + let (_trace_id, response) = router.execute_query(Query::default()).await; assert_eq!(response.status(), 200); assert_eq!( response.json::().await?, diff --git a/apollo-router/tests/integration/subscription.rs b/apollo-router/tests/integration/subscription.rs index 74c42f2034..faad126f8e 100644 --- a/apollo-router/tests/integration/subscription.rs +++ b/apollo-router/tests/integration/subscription.rs @@ -4,7 +4,8 @@ use http::HeaderValue; use serde_json::json; use tower::BoxError; -use super::common::{IntegrationTest, Query}; +use super::common::IntegrationTest; +use super::common::Query; use super::common::Telemetry; const SUBSCRIPTION_CONFIG: &str = include_str!("../fixtures/subscription.router.yaml"); @@ -59,7 +60,10 @@ async fn test_subscription_load() -> Result<(), BoxError> { for _ in 0..100 { let (_id, resp) = router - .execute_query(Query::builder().body(json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}})).build(), + .execute_query( + Query::builder() + .body(json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}})) + .build(), ) .await; assert!(resp.status().is_success()); diff --git a/apollo-router/tests/integration/supergraph.rs b/apollo-router/tests/integration/supergraph.rs index 5732b0921d..8d7ae9727b 100644 --- a/apollo-router/tests/integration/supergraph.rs +++ b/apollo-router/tests/integration/supergraph.rs @@ -1,6 +1,6 @@ - use serde_json::json; use tower::BoxError; + use crate::integration::common::Query; use crate::integration::IntegrationTest; @@ -100,7 +100,12 @@ async fn test_supergraph_errors_on_http1_header_that_does_not_fit_inside_buffer( router.assert_started().await; let (_trace_id, response) = router - .execute_query(Query::builder().body(json!({ "query": "{ __typename }"})).header("test-header", "x".repeat(1048576 + 1)).build()) + .execute_query( + Query::builder() + .body(json!({ "query": "{ __typename }"})) + .header("test-header", "x".repeat(1048576 + 1)) + .build(), + ) .await; assert_eq!(response.status(), 431); Ok(()) @@ -122,7 +127,12 @@ async fn test_supergraph_allow_to_change_http1_max_buf_size() -> Result<(), BoxE router.assert_started().await; let (_trace_id, response) = router - .execute_query(Query::builder().body(json!({ "query": "{ __typename }"})).header("test-header", "x".repeat(1048576 + 1)).build()) + .execute_query( + Query::builder() + .body(json!({ "query": "{ __typename }"})) + .header("test-header", "x".repeat(1048576 + 1)) + .build(), + ) .await; assert_eq!(response.status(), 200); assert_eq!( diff --git a/apollo-router/tests/integration/telemetry/datadog.rs b/apollo-router/tests/integration/telemetry/datadog.rs index 591a2702fc..23bbc32652 100644 --- a/apollo-router/tests/integration/telemetry/datadog.rs +++ b/apollo-router/tests/integration/telemetry/datadog.rs @@ -790,7 +790,6 @@ impl Verifier for DatadogTraceSpec { } fn validate_span_kind(&self, trace: &Value, name: &str, kind: &str) -> Result<(), BoxError> { - let binding1 = trace.select_path(&format!( "$..[?(@.meta.['otel.original_name'] == '{}')].meta.['span.kind']", name diff --git a/apollo-router/tests/integration/telemetry/jaeger.rs b/apollo-router/tests/integration/telemetry/jaeger.rs index b18039bf26..2fc5e8a9f6 100644 --- a/apollo-router/tests/integration/telemetry/jaeger.rs +++ b/apollo-router/tests/integration/telemetry/jaeger.rs @@ -9,7 +9,8 @@ use serde_json::json; use serde_json::Value; use tower::BoxError; -use crate::integration::common::{Query, Telemetry}; +use crate::integration::common::Query; +use crate::integration::common::Telemetry; use crate::integration::telemetry::verifier::Verifier; use crate::integration::telemetry::TraceSpec; use crate::integration::IntegrationTest; @@ -258,31 +259,50 @@ async fn test_span_attributes() -> Result<(), BoxError> { TraceSpec::builder() .services(["client", "router", "subgraph"].into()) .operation_name("ExampleQuery") - .span_attribute("router", [("http.request.method", "POST"), - ("http.response.status_code", "200"), - ("url.path", "/"), - ("http.request.header.x-my-header", "test"), - ("http.request.header.x-not-present", "nope"), - ("http.request.header.x-my-header-condition", "test"), - ("studio.operation.id", "*"), - ].into()) - .span_attribute("supergraph", [ - ("graphql.operation.name", "ExampleQuery"), - ("graphql.operation.type", "query"), - ("graphql.document", "query ExampleQuery {topProducts{name}}"), - ].into()) - .span_attribute("subgraph", [ - ("subgraph.graphql.operation.type", "query"), - ("subgraph.name", "products")].into()) + .span_attribute( + "router", + [ + ("http.request.method", "POST"), + ("http.response.status_code", "200"), + ("url.path", "/"), + ("http.request.header.x-my-header", "test"), + ("http.request.header.x-not-present", "nope"), + ("http.request.header.x-my-header-condition", "test"), + ("studio.operation.id", "*"), + ] + .into(), + ) + .span_attribute( + "supergraph", + [ + ("graphql.operation.name", "ExampleQuery"), + ("graphql.operation.type", "query"), + ("graphql.document", "query ExampleQuery {topProducts{name}}"), + ] + .into(), + ) + .span_attribute( + "subgraph", + [ + ("subgraph.graphql.operation.type", "query"), + ("subgraph.name", "products"), + ] + .into(), + ) .build() - .validate_jaeger_trace(&mut router, Query::builder().header("x-my-header", "test").header("x-my-header-condition", "condition").build()) + .validate_jaeger_trace( + &mut router, + Query::builder() + .header("x-my-header", "test") + .header("x-my-header-condition", "condition") + .build(), + ) .await?; router.graceful_shutdown().await; } Ok(()) } - #[tokio::test(flavor = "multi_thread")] async fn test_decimal_trace_id() -> Result<(), BoxError> { let mut router = IntegrationTest::builder() @@ -327,19 +347,26 @@ impl Verifier for JaegerTraceSpec { fn verify_span_attributes(&self, trace: &Value) -> Result<(), BoxError> { for (span, attributes) in &self.span_attributes { for (key, value) in attributes { - let binding = trace.select_path(&format!("$..spans[?(@.operationName == '{span}')]..tags..[?(@.key == '{key}')].value"))?; + let binding = trace.select_path(&format!( + "$..spans[?(@.operationName == '{span}')]..tags..[?(@.key == '{key}')].value" + ))?; let actual_value = binding .first() - .expect(&format!("could not find attribute {key} on {span}")); + .unwrap_or_else(|| panic!("could not find attribute {key} on {span}")); match actual_value { Value::String(_) if *value == "*" => continue, - Value::String(s) => assert_eq!(s, value, "unexpected attribute {key} on {span}"), + Value::String(s) => { + assert_eq!(s, value, "unexpected attribute {key} on {span}") + } Value::Number(_) if *value == "*" => continue, - Value::Number(n) => assert_eq!(n.to_string(), *value, "unexpected attribute {key} on {span}"), + Value::Number(n) => assert_eq!( + n.to_string(), + *value, + "unexpected attribute {key} on {span}" + ), _ => panic!("unexpected value type"), } - } } Ok(()) diff --git a/apollo-router/tests/integration/telemetry/logging.rs b/apollo-router/tests/integration/telemetry/logging.rs index 21c19a3246..59dc1c7ccd 100644 --- a/apollo-router/tests/integration/telemetry/logging.rs +++ b/apollo-router/tests/integration/telemetry/logging.rs @@ -1,8 +1,9 @@ use tower::BoxError; use uuid::Uuid; -use crate::integration::common::{graph_os_enabled, Query}; +use crate::integration::common::graph_os_enabled; use crate::integration::common::IntegrationTest; +use crate::integration::common::Query; use crate::integration::common::Telemetry; #[tokio::test(flavor = "multi_thread")] diff --git a/apollo-router/tests/integration/telemetry/metrics.rs b/apollo-router/tests/integration/telemetry/metrics.rs index f63613fa00..56a5d6223d 100644 --- a/apollo-router/tests/integration/telemetry/metrics.rs +++ b/apollo-router/tests/integration/telemetry/metrics.rs @@ -2,7 +2,8 @@ use std::time::Duration; use serde_json::json; -use crate::integration::common::{graph_os_enabled, Query}; +use crate::integration::common::graph_os_enabled; +use crate::integration::common::Query; use crate::integration::IntegrationTest; const PROMETHEUS_CONFIG: &str = include_str!("fixtures/prometheus.router.yaml"); @@ -106,9 +107,7 @@ async fn test_subgraph_auth_metrics() { router.update_config(PROMETHEUS_CONFIG).await; router.assert_reloaded().await; // This one will not be signed, counters shouldn't increment. - router - .execute_query(Query::default()) - .await; + router.execute_query(Query::default()).await; // Get Prometheus metrics. let metrics_response = router.get_metrics_response().await.unwrap(); @@ -267,11 +266,7 @@ async fn test_gauges_on_reload() { router.execute_default_query().await; // Introspection query - router - .execute_query(Query::introspection() - - ) - .await; + router.execute_query(Query::introspection()).await; // Persisted query router diff --git a/apollo-router/tests/integration/telemetry/mod.rs b/apollo-router/tests/integration/telemetry/mod.rs index 2769f18d9e..c814faa7ff 100644 --- a/apollo-router/tests/integration/telemetry/mod.rs +++ b/apollo-router/tests/integration/telemetry/mod.rs @@ -1,4 +1,5 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; +use std::collections::HashSet; #[cfg(any(not(feature = "ci"), all(target_arch = "x86_64", target_os = "linux")))] mod datadog; @@ -8,11 +9,10 @@ mod logging; mod metrics; mod otlp; mod propagation; +mod verifier; #[cfg(any(not(feature = "ci"), all(target_arch = "x86_64", target_os = "linux")))] mod zipkin; -mod verifier; -#[derive(buildstructor::Builder)] struct TraceSpec { operation_name: Option, version: Option, @@ -22,7 +22,34 @@ struct TraceSpec { unmeasured_spans: HashSet<&'static str>, priority_sampled: Option<&'static str>, subgraph_sampled: Option, - span_attributes: HashMap<&'static str, Vec<(&'static str, &'static str)>> + span_attributes: HashMap<&'static str, Vec<(&'static str, &'static str)>>, } - +#[buildstructor::buildstructor] +impl TraceSpec { + #[allow(clippy::too_many_arguments)] + #[builder] + pub fn new( + operation_name: Option, + version: Option, + services: Vec<&'static str>, + span_names: HashSet<&'static str>, + measured_spans: HashSet<&'static str>, + unmeasured_spans: HashSet<&'static str>, + priority_sampled: Option<&'static str>, + subgraph_sampled: Option, + span_attributes: HashMap<&'static str, Vec<(&'static str, &'static str)>>, + ) -> Self { + Self { + operation_name, + version, + services, + span_names, + measured_spans, + unmeasured_spans, + priority_sampled, + subgraph_sampled, + span_attributes, + } + } +} diff --git a/apollo-router/tests/integration/telemetry/otlp.rs b/apollo-router/tests/integration/telemetry/otlp.rs index 6947879116..02a7ce3093 100644 --- a/apollo-router/tests/integration/telemetry/otlp.rs +++ b/apollo-router/tests/integration/telemetry/otlp.rs @@ -16,11 +16,12 @@ use wiremock::Mock; use wiremock::MockServer; use wiremock::ResponseTemplate; +use crate::integration::common::graph_os_enabled; +use crate::integration::common::Query; use crate::integration::common::Telemetry; -use crate::integration::common::{graph_os_enabled, Query}; -use crate::integration::IntegrationTest; -use crate::integration::telemetry::TraceSpec; use crate::integration::telemetry::verifier::Verifier; +use crate::integration::telemetry::TraceSpec; +use crate::integration::IntegrationTest; use crate::integration::ValueExt; #[tokio::test(flavor = "multi_thread")] @@ -62,11 +63,7 @@ async fn test_basic() -> Result<(), BoxError> { ) .subgraph_sampled(true) .build() - .validate_otlp_trace( - &mut router, - &mock_server, - Query::default(), - ) + .validate_otlp_trace(&mut router, &mock_server, Query::default()) .await?; TraceSpec::builder() .service("router") @@ -105,11 +102,7 @@ async fn test_otlp_request_with_datadog_propagator() -> Result<(), BoxError> { .priority_sampled("1") .subgraph_sampled(true) .build() - .validate_otlp_trace( - &mut router, - &mock_server, - Query::default(), - ) + .validate_otlp_trace(&mut router, &mock_server, Query::default()) .await?; router.graceful_shutdown().await; Ok(()) @@ -277,12 +270,14 @@ async fn test_untraced_request_no_sample_datadog_agent() -> Result<(), BoxError> let mock_server = mock_otlp_server().await; let config = include_str!("fixtures/otlp_datadog_agent_no_sample.router.yaml") .replace("", &mock_server.uri()); - let mut router = IntegrationTest::builder().config(&config) + let mut router = IntegrationTest::builder() + .config(&config) .telemetry(Telemetry::Otlp { endpoint: Some(format!("{}/v1/traces", mock_server.uri())), }) .extra_propagator(Telemetry::Datadog) - .build().await; + .build() + .await; router.start().await; router.assert_started().await; @@ -316,7 +311,8 @@ async fn test_untraced_request_sample_datadog_agent() -> Result<(), BoxError> { endpoint: Some(format!("{}/v1/traces", mock_server.uri())), }) .extra_propagator(Telemetry::Datadog) - .build().await; + .build() + .await; router.start().await; router.assert_started().await; @@ -542,7 +538,7 @@ async fn test_priority_sampling_no_parent_propagated() -> Result<(), BoxError> { struct OtlpTraceSpec<'a> { trace_spec: TraceSpec, - mock_server: &'a MockServer + mock_server: &'a MockServer, } impl Deref for OtlpTraceSpec<'_> { type Target = TraceSpec; @@ -552,7 +548,6 @@ impl Deref for OtlpTraceSpec<'_> { } } - impl Verifier for OtlpTraceSpec<'_> { fn verify_span_attributes(&self, _span: &Value) -> Result<(), BoxError> { // TODO @@ -580,7 +575,8 @@ impl Verifier for OtlpTraceSpec<'_> { } async fn find_valid_metrics(&self) -> Result<(), BoxError> { - let requests = self.mock_server + let requests = self + .mock_server .received_requests() .await .expect("Could not get otlp requests"); @@ -596,7 +592,6 @@ impl Verifier for OtlpTraceSpec<'_> { } } - async fn get_trace(&self, trace_id: TraceId) -> Result { let requests = self.mock_server.received_requests().await; let trace = Value::Array(requests.unwrap_or_default().iter().filter(|r| r.url.path().ends_with("/traces")) @@ -642,8 +637,6 @@ impl Verifier for OtlpTraceSpec<'_> { Ok(()) } - - fn verify_services(&self, trace: &Value) -> Result<(), axum::BoxError> { let actual_services: HashSet = trace .select_path("$..resource.attributes..[?(@.key == 'service.name')].value.stringValue")? @@ -713,7 +706,6 @@ impl Verifier for OtlpTraceSpec<'_> { Ok(()) } - fn verify_operation_name(&self, trace: &Value) -> Result<(), BoxError> { if let Some(expected_operation_name) = &self.operation_name { let binding = @@ -752,7 +744,6 @@ impl Verifier for OtlpTraceSpec<'_> { } Ok(()) } - } async fn mock_otlp_server() -> MockServer { @@ -788,18 +779,26 @@ impl DatadogId for TraceId { } } - impl TraceSpec { - async fn validate_otlp_trace(self, router: &mut IntegrationTest, mock_server: &MockServer, query: Query) -> Result<(), BoxError>{ + async fn validate_otlp_trace( + self, + router: &mut IntegrationTest, + mock_server: &MockServer, + query: Query, + ) -> Result<(), BoxError> { OtlpTraceSpec { trace_spec: self, - mock_server - }.validate_trace(router, query).await + mock_server, + } + .validate_trace(router, query) + .await } - async fn validate_otlp_metrics(self, mock_server: &MockServer) -> Result<(), BoxError>{ + async fn validate_otlp_metrics(self, mock_server: &MockServer) -> Result<(), BoxError> { OtlpTraceSpec { trace_spec: self, - mock_server - }.validate_metrics().await + mock_server, + } + .validate_metrics() + .await } -} \ No newline at end of file +} diff --git a/apollo-router/tests/integration/telemetry/propagation.rs b/apollo-router/tests/integration/telemetry/propagation.rs index d1b8258c6f..9505efa558 100644 --- a/apollo-router/tests/integration/telemetry/propagation.rs +++ b/apollo-router/tests/integration/telemetry/propagation.rs @@ -1,9 +1,10 @@ use serde_json::json; use tower::BoxError; +use crate::integration::common::graph_os_enabled; use crate::integration::common::IntegrationTest; +use crate::integration::common::Query; use crate::integration::common::Telemetry; -use crate::integration::common::{graph_os_enabled, Query}; #[tokio::test(flavor = "multi_thread")] async fn test_trace_id_via_header() -> Result<(), BoxError> { diff --git a/apollo-router/tests/integration/telemetry/verifier.rs b/apollo-router/tests/integration/telemetry/verifier.rs index bf4ffe4d70..59cbdf2683 100644 --- a/apollo-router/tests/integration/telemetry/verifier.rs +++ b/apollo-router/tests/integration/telemetry/verifier.rs @@ -1,15 +1,22 @@ -use crate::integration::common::Query; -use crate::integration::telemetry::TraceSpec; -use crate::integration::IntegrationTest; -use opentelemetry_api::trace::{SpanContext, TraceId}; -use serde_json::Value; use std::time::Duration; + use anyhow::anyhow; +use opentelemetry_api::trace::SpanContext; +use opentelemetry_api::trace::TraceId; +use serde_json::Value; use tower::BoxError; +use crate::integration::common::Query; +use crate::integration::telemetry::TraceSpec; +use crate::integration::IntegrationTest; + pub trait Verifier { fn spec(&self) -> &TraceSpec; - async fn validate_trace(&self, router: &mut IntegrationTest, query: Query) -> Result<(), BoxError> { + async fn validate_trace( + &self, + router: &mut IntegrationTest, + query: Query, + ) -> Result<(), BoxError> { let (id, response) = router.execute_query(query).await; for _ in 0..20 { if self.find_valid_trace(id).await.is_ok() { @@ -22,7 +29,6 @@ pub trait Verifier { assert!(response.status().is_success()); self.validate_subgraph(subgraph_context)?; Ok(()) - } async fn validate_metrics(&self) -> Result<(), BoxError> { @@ -40,18 +46,12 @@ pub trait Verifier { unimplemented!("find_valid_metrics") } - fn validate_subgraph( - &self, - subgraph_context: SpanContext, - ) -> Result<(), BoxError> { + fn validate_subgraph(&self, subgraph_context: SpanContext) -> Result<(), BoxError> { self.validate_subgraph_priority_sampled(&subgraph_context)?; self.validate_subgraph_sampled(&subgraph_context)?; Ok(()) } - fn validate_subgraph_sampled( - &self, - subgraph_context: &SpanContext, - ) -> Result<(), BoxError> { + fn validate_subgraph_sampled(&self, subgraph_context: &SpanContext) -> Result<(), BoxError> { if let Some(sampled) = self.spec().priority_sampled { assert_eq!( subgraph_context.trace_state().get("psr"), @@ -60,22 +60,19 @@ pub trait Verifier { ); } - Ok(()) } fn validate_subgraph_priority_sampled( &self, subgraph_context: &SpanContext, - ) -> Result<(), BoxError>{ + ) -> Result<(), BoxError> { if let Some(sampled) = self.spec().subgraph_sampled { assert_eq!(subgraph_context.is_sampled(), sampled, "subgraph sampled"); } Ok(()) } - - #[allow(clippy::too_many_arguments)] async fn find_valid_trace(&self, trace_id: TraceId) -> Result<(), BoxError> { // A valid trace has: @@ -156,5 +153,4 @@ pub trait Verifier { fn verify_operation_name(&self, trace: &Value) -> Result<(), BoxError>; fn verify_priority_sampled(&self, trace: &Value) -> Result<(), BoxError>; - } diff --git a/apollo-router/tests/integration/telemetry/zipkin.rs b/apollo-router/tests/integration/telemetry/zipkin.rs index 460c10add4..45f51620d0 100644 --- a/apollo-router/tests/integration/telemetry/zipkin.rs +++ b/apollo-router/tests/integration/telemetry/zipkin.rs @@ -3,16 +3,18 @@ extern crate core; use std::collections::HashSet; use std::ops::Deref; -use crate::integration::common::{Query, Telemetry}; -use crate::integration::telemetry::verifier::Verifier; -use crate::integration::telemetry::TraceSpec; -use crate::integration::IntegrationTest; -use crate::integration::ValueExt; use anyhow::anyhow; use opentelemetry_api::trace::TraceId; use serde_json::Value; use tower::BoxError; +use crate::integration::common::Query; +use crate::integration::common::Telemetry; +use crate::integration::telemetry::verifier::Verifier; +use crate::integration::telemetry::TraceSpec; +use crate::integration::IntegrationTest; +use crate::integration::ValueExt; + #[tokio::test(flavor = "multi_thread")] async fn test_basic() -> Result<(), BoxError> { let mut router = IntegrationTest::builder() @@ -38,7 +40,6 @@ async fn test_basic() -> Result<(), BoxError> { Ok(()) } - struct ZipkinTraceSpec { trace_spec: TraceSpec, } @@ -55,11 +56,9 @@ impl Verifier for ZipkinTraceSpec { Ok(()) } fn verify_version(&self, _trace: &Value) -> Result<(), BoxError> { - Ok(()) } - fn measured_span(&self, _trace: &Value, _name: &str) -> Result { Ok(true) } @@ -76,7 +75,9 @@ impl Verifier for ZipkinTraceSpec { .collect(); tracing::debug!("found services {:?}", actual_services); - let expected_services = self.trace_spec.services + let expected_services = self + .trace_spec + .services .iter() .map(|s| s.to_string()) .collect::>(); @@ -98,8 +99,8 @@ impl Verifier for ZipkinTraceSpec { fn verify_operation_name(&self, trace: &Value) -> Result<(), BoxError> { if let Some(expected_operation_name) = &self.operation_name { - let binding = - trace.select_path("$..[?(@.name == 'supergraph')].tags..['graphql.operation.name']")?; + let binding = trace + .select_path("$..[?(@.name == 'supergraph')].tags..['graphql.operation.name']")?; let operation_name = binding.first(); assert_eq!( operation_name @@ -116,10 +117,15 @@ impl Verifier for ZipkinTraceSpec { Ok(()) } - async fn get_trace(&self, trace_id: TraceId) -> Result { let params = url::form_urlencoded::Serializer::new(String::new()) - .append_pair("service", self.trace_spec.services.first().expect("expected root service")) + .append_pair( + "service", + self.trace_spec + .services + .first() + .expect("expected root service"), + ) .finish(); let id = trace_id.to_string(); diff --git a/apollo-router/tests/integration/traffic_shaping.rs b/apollo-router/tests/integration/traffic_shaping.rs index 5d8b45e28b..579cb2b2a5 100644 --- a/apollo-router/tests/integration/traffic_shaping.rs +++ b/apollo-router/tests/integration/traffic_shaping.rs @@ -5,7 +5,8 @@ use serde_json::json; use tower::BoxError; use wiremock::ResponseTemplate; -use crate::integration::common::{graph_os_enabled, Query}; +use crate::integration::common::graph_os_enabled; +use crate::integration::common::Query; use crate::integration::common::Telemetry; use crate::integration::IntegrationTest; @@ -99,9 +100,13 @@ async fn test_router_timeout_operation_name_in_tracing() -> Result<(), BoxError> router.assert_started().await; let (_trace_id, response) = router - .execute_query(Query::builder().body(json!({ - "query": "query UniqueName { topProducts { name } }" - })).build()) + .execute_query( + Query::builder() + .body(json!({ + "query": "query UniqueName { topProducts { name } }" + })) + .build(), + ) .await; assert_eq!(response.status(), 504); let response = response.text().await?; diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index abc45e2ff1..5beba9d4b5 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -30,6 +30,7 @@ use wiremock::ResponseTemplate; #[path = "./common.rs"] pub(crate) mod common; pub(crate) use common::IntegrationTest; + use crate::common::Query; fn main() -> Result> { @@ -498,7 +499,12 @@ impl TestExecution { writeln!(out, "header: {:?}\n", headers).unwrap(); let (_, response) = router - .execute_query(Query::builder().body(request).headers(headers.clone()).build()) + .execute_query( + Query::builder() + .body(request) + .headers(headers.clone()) + .build(), + ) .await; writeln!(out, "response headers: {:?}", response.headers()).unwrap(); From 437f0531859c94494da519c146f0710814e71a1c Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 13 Dec 2024 14:13:10 +0000 Subject: [PATCH 27/35] Fix a couple of new tests from merge --- apollo-router/tests/integration/supergraph.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/apollo-router/tests/integration/supergraph.rs b/apollo-router/tests/integration/supergraph.rs index 8d7ae9727b..07b4c81089 100644 --- a/apollo-router/tests/integration/supergraph.rs +++ b/apollo-router/tests/integration/supergraph.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "hyper_header_limits")] +use std::collections::HashMap; + use serde_json::json; use tower::BoxError; @@ -45,7 +48,12 @@ async fn test_supergraph_errors_on_http1_max_headers() -> Result<(), BoxError> { } let (_trace_id, response) = router - .execute_query_with_headers(&json!({ "query": "{ __typename }"}), headers) + .execute_query( + Query::builder() + .body(json!({ "query": "{ __typename }"})) + .headers(headers) + .build(), + ) .await; assert_eq!(response.status(), 431); Ok(()) @@ -73,7 +81,12 @@ async fn test_supergraph_allow_to_change_http1_max_headers() -> Result<(), BoxEr } let (_trace_id, response) = router - .execute_query_with_headers(&json!({ "query": "{ __typename }"}), headers) + .execute_query( + Query::builder() + .body(json!({ "query": "{ __typename }"})) + .headers(headers) + .build(), + ) .await; assert_eq!(response.status(), 200); assert_eq!( From e701fda2126a06f6301fb068febb8c3d5758d10d Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 13 Dec 2024 14:42:14 +0000 Subject: [PATCH 28/35] Make test less flaky --- apollo-router/tests/integration/telemetry/jaeger.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apollo-router/tests/integration/telemetry/jaeger.rs b/apollo-router/tests/integration/telemetry/jaeger.rs index 2fc5e8a9f6..8c38c59ec2 100644 --- a/apollo-router/tests/integration/telemetry/jaeger.rs +++ b/apollo-router/tests/integration/telemetry/jaeger.rs @@ -482,8 +482,10 @@ impl Verifier for JaegerTraceSpec { if let Some(expected_operation_name) = &self.operation_name { let binding = trace.select_path("$..spans[?(@.operationName == 'supergraph')]..tags[?(@.key == 'graphql.operation.name')].value")?; - println!("binding: {:?}", binding); let operation_name = binding.first(); + if operation_name.is_none() { + return Err(BoxError::from("graphql.operation.name not found")); + } assert_eq!( operation_name .expect("graphql.operation.name expected") From be6842057cca4bcca5df27727bd35320b84b5e96 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 13 Dec 2024 20:09:13 +0000 Subject: [PATCH 29/35] Revert change to propagator order --- apollo-router/src/plugins/telemetry/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index 95b8083525..e70b2cf55c 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -947,18 +947,21 @@ impl Telemetry { if propagation.zipkin || tracing.zipkin.enabled { propagators.push(Box::::default()); } + if propagation.datadog || tracing.datadog.enabled() { + propagators.push(Box::::default()); + } if propagation.aws_xray { propagators.push(Box::::default()); } + + // This propagator MUST come last because the user is trying to override the default behavior of the + // other propagators. if let Some(from_request_header) = &propagation.request.header_name { propagators.push(Box::new(CustomTraceIdPropagator::new( from_request_header.to_string(), propagation.request.format.clone(), ))); } - if propagation.datadog || tracing.datadog.enabled() { - propagators.push(Box::::default()); - } TextMapCompositePropagator::new(propagators) } From 1e6b5593246773c246822980f2a8e428d8d0a488 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 13 Dec 2024 20:27:26 +0000 Subject: [PATCH 30/35] Add test for custom propagation overriding the DD propagator --- .../tests/integration/telemetry/datadog.rs | 33 +++++++++++++++++++ ...dog_header_propagator_override.router.yaml | 29 ++++++++++++++++ .../tests/integration/telemetry/mod.rs | 3 ++ .../tests/integration/telemetry/verifier.rs | 3 ++ 4 files changed, 68 insertions(+) create mode 100644 apollo-router/tests/integration/telemetry/fixtures/datadog_header_propagator_override.router.yaml diff --git a/apollo-router/tests/integration/telemetry/datadog.rs b/apollo-router/tests/integration/telemetry/datadog.rs index 23bbc32652..b0427ec691 100644 --- a/apollo-router/tests/integration/telemetry/datadog.rs +++ b/apollo-router/tests/integration/telemetry/datadog.rs @@ -439,6 +439,39 @@ async fn test_override_span_names_late() -> Result<(), BoxError> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_header_propagator_override() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Datadog) + .config(include_str!( + "fixtures/datadog_header_propagator_override.router.yaml" + )) + .build() + .await; + + router.start().await; + router.assert_started().await; + TraceSpec::builder() + .services(["router", "subgraph"].into()) + .subgraph_sampled(true) + .trace_id("00000000000000000000000000000001") + .build() + .validate_datadog_trace( + &mut router, + Query::builder() + .header("trace-id", "00000000000000000000000000000001") + .header("x-datadog-trace-id", "2") + .traced(false) + .build(), + ) + .await?; + router.graceful_shutdown().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn test_basic() -> Result<(), BoxError> { if !graph_os_enabled() { diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_header_propagator_override.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_header_propagator_override.router.yaml new file mode 100644 index 0000000000..595639f1ff --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_header_propagator_override.router.yaml @@ -0,0 +1,29 @@ +telemetry: + exporters: + tracing: + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + format: datadog + propagation: + datadog: true + request: + header_name: trace-id + common: + service_name: router + parent_based_sampler: false + resource: + env: local1 + service.version: router_version_override + preview_datadog_agent_sampling: true + datadog: + enabled: true + batch_processor: + scheduled_delay: 100ms + instrumentation: + spans: + mode: spec_compliant + supergraph: + attributes: + graphql.operation.name: true + diff --git a/apollo-router/tests/integration/telemetry/mod.rs b/apollo-router/tests/integration/telemetry/mod.rs index c814faa7ff..4edf023702 100644 --- a/apollo-router/tests/integration/telemetry/mod.rs +++ b/apollo-router/tests/integration/telemetry/mod.rs @@ -22,6 +22,7 @@ struct TraceSpec { unmeasured_spans: HashSet<&'static str>, priority_sampled: Option<&'static str>, subgraph_sampled: Option, + trace_id: Option<&'static str>, span_attributes: HashMap<&'static str, Vec<(&'static str, &'static str)>>, } @@ -38,6 +39,7 @@ impl TraceSpec { unmeasured_spans: HashSet<&'static str>, priority_sampled: Option<&'static str>, subgraph_sampled: Option, + trace_id: Option<&'static str>, span_attributes: HashMap<&'static str, Vec<(&'static str, &'static str)>>, ) -> Self { Self { @@ -50,6 +52,7 @@ impl TraceSpec { priority_sampled, subgraph_sampled, span_attributes, + trace_id, } } } diff --git a/apollo-router/tests/integration/telemetry/verifier.rs b/apollo-router/tests/integration/telemetry/verifier.rs index 59cbdf2683..c6f92cddef 100644 --- a/apollo-router/tests/integration/telemetry/verifier.rs +++ b/apollo-router/tests/integration/telemetry/verifier.rs @@ -18,6 +18,9 @@ pub trait Verifier { query: Query, ) -> Result<(), BoxError> { let (id, response) = router.execute_query(query).await; + if let Some(spec_id) = self.spec().trace_id { + assert_eq!(id.to_string(), spec_id, "trace id"); + } for _ in 0..20 { if self.find_valid_trace(id).await.is_ok() { break; From 55ec86e4d875a19caf4ac5ee5726425836c2ca70 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 13 Dec 2024 21:45:46 +0000 Subject: [PATCH 31/35] Add test and fix datadog propagator to fall back to the span sampling if psr is not in context.. --- .../telemetry/tracing/datadog_exporter/mod.rs | 8 ++++- .../tests/integration/telemetry/datadog.rs | 30 +++++++++++++++++++ ...adog_agent_sampling_disabled_1.router.yaml | 22 ++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 apollo-router/tests/integration/telemetry/fixtures/datadog_agent_sampling_disabled_1.router.yaml diff --git a/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/mod.rs b/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/mod.rs index 74907ee6a4..c8ee8c4425 100644 --- a/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/mod.rs +++ b/apollo-router/src/plugins/telemetry/tracing/datadog_exporter/mod.rs @@ -450,7 +450,13 @@ pub(crate) mod propagator { let sampling_priority = span_context .trace_state() .sampling_priority() - .unwrap_or_default(); + .unwrap_or_else(|| { + if span_context.is_sampled() { + SamplingPriority::AutoKeep + } else { + SamplingPriority::AutoReject + } + }); injector.set( DATADOG_SAMPLING_PRIORITY_HEADER, (sampling_priority as i32).to_string(), diff --git a/apollo-router/tests/integration/telemetry/datadog.rs b/apollo-router/tests/integration/telemetry/datadog.rs index b0427ec691..95d6117092 100644 --- a/apollo-router/tests/integration/telemetry/datadog.rs +++ b/apollo-router/tests/integration/telemetry/datadog.rs @@ -70,6 +70,36 @@ async fn test_sampling_datadog_agent_disabled() -> Result<(), BoxError> { Ok(()) } + + +// We want to check we're able to override the behavior of preview_datadog_agent_sampling configuration even if we set a datadog exporter +#[tokio::test(flavor = "multi_thread")] +async fn test_sampling_datadog_agent_disabled_always_sample() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Datadog) + .config(include_str!( + "fixtures/datadog_agent_sampling_disabled_1.router.yaml" + )) + .build() + .await; + + router.start().await; + router.assert_started().await; + + TraceSpec::builder() + .services(["router", "subgraph"].into()) + .subgraph_sampled(true) + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(false).build()) + .await?; + router.graceful_shutdown().await; + + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn test_priority_sampling_propagated() -> Result<(), BoxError> { if !graph_os_enabled() { diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_agent_sampling_disabled_1.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_agent_sampling_disabled_1.router.yaml new file mode 100644 index 0000000000..2334508de4 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_agent_sampling_disabled_1.router.yaml @@ -0,0 +1,22 @@ +telemetry: + apollo: + field_level_instrumentation_sampler: always_off + exporters: + tracing: + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + common: + service_name: router + sampler: 1.0 + preview_datadog_agent_sampling: false + datadog: + enabled: true + batch_processor: + scheduled_delay: 100ms + fixed_span_names: false + enable_span_mapping: false + instrumentation: + spans: + mode: spec_compliant + From 3b5d256251128c72d85b5473bf64d34a39d464c3 Mon Sep 17 00:00:00 2001 From: bryn Date: Fri, 13 Dec 2024 21:51:42 +0000 Subject: [PATCH 32/35] Add test for non datadog agent propagation zero percent sampling. --- .../tests/integration/telemetry/datadog.rs | 49 +++++++++++++++++-- ...adog_agent_sampling_disabled_0.router.yaml | 22 +++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 apollo-router/tests/integration/telemetry/fixtures/datadog_agent_sampling_disabled_0.router.yaml diff --git a/apollo-router/tests/integration/telemetry/datadog.rs b/apollo-router/tests/integration/telemetry/datadog.rs index 95d6117092..2a58132927 100644 --- a/apollo-router/tests/integration/telemetry/datadog.rs +++ b/apollo-router/tests/integration/telemetry/datadog.rs @@ -70,8 +70,6 @@ async fn test_sampling_datadog_agent_disabled() -> Result<(), BoxError> { Ok(()) } - - // We want to check we're able to override the behavior of preview_datadog_agent_sampling configuration even if we set a datadog exporter #[tokio::test(flavor = "multi_thread")] async fn test_sampling_datadog_agent_disabled_always_sample() -> Result<(), BoxError> { @@ -92,9 +90,53 @@ async fn test_sampling_datadog_agent_disabled_always_sample() -> Result<(), BoxE TraceSpec::builder() .services(["router", "subgraph"].into()) .subgraph_sampled(true) + .priority_sampled("1") + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(false).build()) + .await?; + + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .subgraph_sampled(true) + .priority_sampled("1") + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).build()) + .await?; + router.graceful_shutdown().await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_sampling_datadog_agent_disabled_never_sample() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Datadog) + .config(include_str!( + "fixtures/datadog_agent_sampling_disabled_0.router.yaml" + )) + .build() + .await; + + router.start().await; + router.assert_started().await; + + TraceSpec::builder() + .services([].into()) + .subgraph_sampled(false) .build() .validate_datadog_trace(&mut router, Query::builder().traced(false).build()) .await?; + + TraceSpec::builder() + .services(["client", "router", "subgraph"].into()) + .subgraph_sampled(true) + .priority_sampled("1") + .build() + .validate_datadog_trace(&mut router, Query::builder().traced(true).build()) + .await?; router.graceful_shutdown().await; Ok(()) @@ -911,7 +953,8 @@ impl Verifier for DatadogTraceSpec { .as_f64() .expect("psr not string") .to_string(), - psr + psr, + "psr mismatch" ); } } diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_agent_sampling_disabled_0.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_agent_sampling_disabled_0.router.yaml new file mode 100644 index 0000000000..42f56dd642 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_agent_sampling_disabled_0.router.yaml @@ -0,0 +1,22 @@ +telemetry: + apollo: + field_level_instrumentation_sampler: always_off + exporters: + tracing: + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + common: + service_name: router + sampler: 0.0 + preview_datadog_agent_sampling: false + datadog: + enabled: true + batch_processor: + scheduled_delay: 100ms + fixed_span_names: false + enable_span_mapping: false + instrumentation: + spans: + mode: spec_compliant + From 0632ee6e6881919d9885e95483fce6b11474a148 Mon Sep 17 00:00:00 2001 From: bryn Date: Sun, 15 Dec 2024 22:38:29 +0000 Subject: [PATCH 33/35] Improve trace_id test --- .../tests/integration/telemetry/datadog.rs | 80 ++++++++++++++++--- .../telemetry/fixtures/datadog.router.yaml | 3 - .../datadog_no_parent_sampler.router.yaml | 3 - ...adog_parent_sampler_very_small.router.yaml | 5 +- ...t_sampler_very_small_no_parent.router.yaml | 25 ++++++ .../tests/integration/telemetry/mod.rs | 17 +++- .../tests/integration/telemetry/otlp.rs | 11 +-- .../tests/integration/telemetry/verifier.rs | 4 +- 8 files changed, 112 insertions(+), 36 deletions(-) create mode 100644 apollo-router/tests/integration/telemetry/fixtures/datadog_parent_sampler_very_small_no_parent.router.yaml diff --git a/apollo-router/tests/integration/telemetry/datadog.rs b/apollo-router/tests/integration/telemetry/datadog.rs index 2a58132927..db33307ae9 100644 --- a/apollo-router/tests/integration/telemetry/datadog.rs +++ b/apollo-router/tests/integration/telemetry/datadog.rs @@ -12,6 +12,7 @@ use crate::integration::common::graph_os_enabled; use crate::integration::common::Query; use crate::integration::common::Telemetry; use crate::integration::telemetry::verifier::Verifier; +use crate::integration::telemetry::DatadogId; use crate::integration::telemetry::TraceSpec; use crate::integration::IntegrationTest; use crate::integration::ValueExt; @@ -357,6 +358,69 @@ async fn test_priority_sampling_parent_sampler_very_small() -> Result<(), BoxErr Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_priority_sampling_parent_sampler_very_small_no_parent() -> Result<(), BoxError> { + // Note that there is a very small chance this test will fail. We are trying to test a non-zero sampler. + + if !graph_os_enabled() { + return Ok(()); + } + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Datadog) + .config(include_str!( + "fixtures/datadog_parent_sampler_very_small_no_parent.router.yaml" + )) + .build() + .await; + + router.start().await; + router.assert_started().await; + + // // The router should respect upstream but also almost never sample if left to its own devices. + TraceSpec::builder() + .services(["client", "router"].into()) + .priority_sampled("0") + .subgraph_sampled(false) + .build() + .validate_datadog_trace(&mut router, Query::builder().psr("-1").traced(true).build()) + .await?; + TraceSpec::builder() + .services(["client", "router"].into()) + .priority_sampled("0") + .subgraph_sampled(false) + .build() + .validate_datadog_trace(&mut router, Query::builder().psr("0").traced(true).build()) + .await?; + + TraceSpec::builder() + .services(["client", "router"].into()) + .priority_sampled("0") + .subgraph_sampled(false) + .build() + .validate_datadog_trace(&mut router, Query::builder().psr("1").traced(true).build()) + .await?; + + TraceSpec::builder() + .services(["client", "router"].into()) + .priority_sampled("0") + .subgraph_sampled(false) + .build() + .validate_datadog_trace(&mut router, Query::builder().psr("2").traced(true).build()) + .await?; + + TraceSpec::builder() + .services(["router"].into()) + .priority_sampled("0") + .subgraph_sampled(false) + .build() + .validate_datadog_trace(&mut router, Query::builder().psr("2").traced(false).build()) + .await?; + + router.graceful_shutdown().await; + + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn test_untraced_request() -> Result<(), BoxError> { if !graph_os_enabled() { @@ -524,17 +588,19 @@ async fn test_header_propagator_override() -> Result<(), BoxError> { .build() .await; + let trace_id = opentelemetry::trace::TraceId::from_u128(uuid::Uuid::new_v4().as_u128()); + router.start().await; router.assert_started().await; TraceSpec::builder() .services(["router", "subgraph"].into()) .subgraph_sampled(true) - .trace_id("00000000000000000000000000000001") + .trace_id(format!("{:032x}", trace_id.to_datadog())) .build() .validate_datadog_trace( &mut router, Query::builder() - .header("trace-id", "00000000000000000000000000000001") + .header("trace-id", trace_id.to_string()) .header("x-datadog-trace-id", "2") .traced(false) .build(), @@ -779,16 +845,6 @@ async fn test_span_metrics() -> Result<(), BoxError> { Ok(()) } -pub(crate) trait DatadogId { - fn to_datadog(&self) -> String; -} -impl DatadogId for TraceId { - fn to_datadog(&self) -> String { - let bytes = &self.to_bytes()[std::mem::size_of::()..std::mem::size_of::()]; - u64::from_be_bytes(bytes.try_into().unwrap()).to_string() - } -} - struct DatadogTraceSpec { trace_spec: TraceSpec, } diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog.router.yaml index 0f0f50dd78..c1c4b2096e 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/datadog.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog.router.yaml @@ -5,9 +5,6 @@ telemetry: enabled: true header_name: apollo-custom-trace-id format: datadog - propagation: - trace_context: true - jaeger: true common: service_name: router resource: diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_no_parent_sampler.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_no_parent_sampler.router.yaml index 2e9c634dd9..c6ec7c22b7 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/datadog_no_parent_sampler.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_no_parent_sampler.router.yaml @@ -5,9 +5,6 @@ telemetry: enabled: true header_name: apollo-custom-trace-id format: datadog - propagation: - trace_context: true - jaeger: true common: service_name: router parent_based_sampler: false diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_parent_sampler_very_small.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_parent_sampler_very_small.router.yaml index 90a5594503..206e72d1b1 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/datadog_parent_sampler_very_small.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_parent_sampler_very_small.router.yaml @@ -5,12 +5,9 @@ telemetry: enabled: true header_name: apollo-custom-trace-id format: datadog - propagation: - trace_context: true - jaeger: true common: service_name: router - sampler: 0.000000001 + sampler: 0.00001 parent_based_sampler: true resource: env: local1 diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_parent_sampler_very_small_no_parent.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_parent_sampler_very_small_no_parent.router.yaml new file mode 100644 index 0000000000..658b7d2361 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_parent_sampler_very_small_no_parent.router.yaml @@ -0,0 +1,25 @@ +telemetry: + exporters: + tracing: + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + format: datadog + common: + service_name: router + sampler: 0.00001 + parent_based_sampler: false + resource: + env: local1 + service.version: router_version_override + preview_datadog_agent_sampling: true + datadog: + enabled: true + batch_processor: + scheduled_delay: 100ms + instrumentation: + spans: + mode: spec_compliant + supergraph: + attributes: + graphql.operation.name: true diff --git a/apollo-router/tests/integration/telemetry/mod.rs b/apollo-router/tests/integration/telemetry/mod.rs index 4edf023702..6319182e62 100644 --- a/apollo-router/tests/integration/telemetry/mod.rs +++ b/apollo-router/tests/integration/telemetry/mod.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; use std::collections::HashSet; +use opentelemetry_api::trace::TraceId; + #[cfg(any(not(feature = "ci"), all(target_arch = "x86_64", target_os = "linux")))] mod datadog; #[cfg(any(not(feature = "ci"), all(target_arch = "x86_64", target_os = "linux")))] @@ -22,7 +24,7 @@ struct TraceSpec { unmeasured_spans: HashSet<&'static str>, priority_sampled: Option<&'static str>, subgraph_sampled: Option, - trace_id: Option<&'static str>, + trace_id: Option, span_attributes: HashMap<&'static str, Vec<(&'static str, &'static str)>>, } @@ -39,7 +41,7 @@ impl TraceSpec { unmeasured_spans: HashSet<&'static str>, priority_sampled: Option<&'static str>, subgraph_sampled: Option, - trace_id: Option<&'static str>, + trace_id: Option, span_attributes: HashMap<&'static str, Vec<(&'static str, &'static str)>>, ) -> Self { Self { @@ -56,3 +58,14 @@ impl TraceSpec { } } } + +#[allow(dead_code)] +pub trait DatadogId { + fn to_datadog(&self) -> u64; +} +impl DatadogId for TraceId { + fn to_datadog(&self) -> u64 { + let bytes = &self.to_bytes()[std::mem::size_of::()..std::mem::size_of::()]; + u64::from_be_bytes(bytes.try_into().unwrap()) + } +} diff --git a/apollo-router/tests/integration/telemetry/otlp.rs b/apollo-router/tests/integration/telemetry/otlp.rs index 02a7ce3093..af73bc32e8 100644 --- a/apollo-router/tests/integration/telemetry/otlp.rs +++ b/apollo-router/tests/integration/telemetry/otlp.rs @@ -20,6 +20,7 @@ use crate::integration::common::graph_os_enabled; use crate::integration::common::Query; use crate::integration::common::Telemetry; use crate::integration::telemetry::verifier::Verifier; +use crate::integration::telemetry::DatadogId; use crate::integration::telemetry::TraceSpec; use crate::integration::IntegrationTest; use crate::integration::ValueExt; @@ -769,16 +770,6 @@ async fn mock_otlp_server() -> MockServer { mock_server } -pub(crate) trait DatadogId { - fn to_datadog(&self) -> u64; -} -impl DatadogId for TraceId { - fn to_datadog(&self) -> u64 { - let bytes = &self.to_bytes()[std::mem::size_of::()..std::mem::size_of::()]; - u64::from_be_bytes(bytes.try_into().unwrap()) - } -} - impl TraceSpec { async fn validate_otlp_trace( self, diff --git a/apollo-router/tests/integration/telemetry/verifier.rs b/apollo-router/tests/integration/telemetry/verifier.rs index c6f92cddef..3fe9fdabbd 100644 --- a/apollo-router/tests/integration/telemetry/verifier.rs +++ b/apollo-router/tests/integration/telemetry/verifier.rs @@ -18,8 +18,8 @@ pub trait Verifier { query: Query, ) -> Result<(), BoxError> { let (id, response) = router.execute_query(query).await; - if let Some(spec_id) = self.spec().trace_id { - assert_eq!(id.to_string(), spec_id, "trace id"); + if let Some(spec_id) = &self.spec().trace_id { + assert_eq!(id.to_string(), *spec_id, "trace id"); } for _ in 0..20 { if self.find_valid_trace(id).await.is_ok() { From 98b66d100377aa19cf309cd4c4fedcf622b4cbaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e?= Date: Mon, 16 Dec 2024 08:57:56 +0000 Subject: [PATCH 34/35] fix(federation): do not use internal errors for context parse errors (#6437) --- apollo-federation/src/error/mod.rs | 4 ++ .../src/query_graph/build_query_graph.rs | 39 ++++++++++++------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index ebafcbbdd6..925eaaadb4 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -133,6 +133,8 @@ pub enum SingleFederationError { UnknownOperation, #[error("Must provide operation name if query contains multiple operations")] OperationNameNotProvided, + #[error(r#"{message} in @fromContext substring "{context}""#)] + FromContextParseError { context: String, message: String }, #[error("Unsupported custom directive @{name} on fragment spread. Due to query transformations during planning, the router requires directives on fragment spreads to support both the FRAGMENT_SPREAD and INLINE_FRAGMENT locations.")] UnsupportedSpreadDirective { name: Name }, #[error("{message}")] @@ -305,6 +307,8 @@ impl SingleFederationError { SingleFederationError::InvalidGraphQL { .. } | SingleFederationError::InvalidGraphQLName(_) => ErrorCode::InvalidGraphQL, SingleFederationError::InvalidSubgraph { .. } => ErrorCode::InvalidGraphQL, + // Technically it's not invalid graphql, but it is invalid syntax inside graphql... + SingleFederationError::FromContextParseError { .. } => ErrorCode::InvalidGraphQL, // TODO(@goto-bus-stop): this should have a different error code: it's not invalid, // just unsupported due to internal limitations. SingleFederationError::UnsupportedSpreadDirective { .. } => ErrorCode::InvalidGraphQL, diff --git a/apollo-federation/src/query_graph/build_query_graph.rs b/apollo-federation/src/query_graph/build_query_graph.rs index 77ff9ec802..b53efce44c 100644 --- a/apollo-federation/src/query_graph/build_query_graph.rs +++ b/apollo-federation/src/query_graph/build_query_graph.rs @@ -19,7 +19,6 @@ use strum::IntoEnumIterator; use crate::bail; use crate::error::FederationError; use crate::error::SingleFederationError; -use crate::internal_error; use crate::link::federation_spec_definition::get_federation_spec_definition_from_subgraph; use crate::link::federation_spec_definition::FederationSpecDefinition; use crate::link::federation_spec_definition::KeyDirectiveArguments; @@ -2363,6 +2362,14 @@ static CONTEXT_PARSING_LEADING_PATTERN: LazyLock = static CONTEXT_PARSING_CONTEXT_PATTERN: LazyLock = LazyLock::new(|| Regex::new(r#"^([A-Za-z_](?-u:\w)*)((?s:.)*)$"#).unwrap()); +fn context_parse_error(context: &str, message: &str) -> FederationError { + SingleFederationError::FromContextParseError { + context: context.to_string(), + message: message.to_string(), + } + .into() +} + fn parse_context(field: &str) -> Result<(String, String), FederationError> { // PORT_NOTE: The original JS regex, as shown below // /^(?:[\n\r\t ,]|#[^\n\r]*(?![^\n\r]))*\$(?:[\n\r\t ,]|#[^\n\r]*(?![^\n\r]))*([A-Za-z_]\w*(?!\w))([\s\S]*)$/ @@ -2379,15 +2386,16 @@ fn parse_context(field: &str) -> Result<(String, String), FederationError> { iter_into_single_item(CONTEXT_PARSING_LEADING_PATTERN.captures_iter(input)) .and_then(|c| c.get(1)) .map(|m| m.as_str()) - .ok_or_else(|| { - internal_error!(r#"Failed to skip any leading ignored tokens in @fromContext substring "{input}""#) - }) + .ok_or_else(|| context_parse_error(input, "Failed to skip any leading ignored tokens")) } let dollar_start = strip_leading_ignored_tokens(field)?; let mut dollar_iter = dollar_start.chars(); if dollar_iter.next() != Some('$') { - bail!(r#"Failed to find leading "$" in @fromContext substring "{dollar_start}""#); + return Err(context_parse_error( + dollar_start, + r#"Failed to find leading "$""#, + )); } let after_dollar = dollar_iter.as_str(); @@ -2395,26 +2403,29 @@ fn parse_context(field: &str) -> Result<(String, String), FederationError> { let Some(context_captures) = iter_into_single_item(CONTEXT_PARSING_CONTEXT_PATTERN.captures_iter(context_start)) else { - bail!( - r#"Failed to find context name token and selection in @fromContext substring "{context_start}""# - ); + return Err(context_parse_error( + dollar_start, + "Failed to find context name token and selection", + )); }; let context = match context_captures.get(1).map(|m| m.as_str()) { Some(context) if !context.is_empty() => context, _ => { - bail!( - r#"Expected to find non-empty context name in @fromContext substring "{context_start}""# - ); + return Err(context_parse_error( + context_start, + "Expected to find non-empty context name", + )); } }; let selection = match context_captures.get(2).map(|m| m.as_str()) { Some(selection) if !selection.is_empty() => selection, _ => { - bail!( - r#"Expected to find non-empty selection in @fromContext substring "{context_start}""# - ); + return Err(context_parse_error( + context_start, + "Expected to find non-empty selection", + )); } }; From 94884d8bf124d4c50b211c136e42667acf3d7624 Mon Sep 17 00:00:00 2001 From: Edward Huang Date: Mon, 16 Dec 2024 14:47:11 -0800 Subject: [PATCH 35/35] docs: clarify subgraph_request (#5128) --- .../reference/router/telemetry/metrics-exporters/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/reference/router/telemetry/metrics-exporters/overview.mdx b/docs/source/reference/router/telemetry/metrics-exporters/overview.mdx index efb22e3ccc..5ca04f389b 100644 --- a/docs/source/reference/router/telemetry/metrics-exporters/overview.mdx +++ b/docs/source/reference/router/telemetry/metrics-exporters/overview.mdx @@ -165,7 +165,7 @@ telemetry: static: # Always apply this attribute to all metrics for all subgraphs - name: kind - value: subgraph_request + value: subgraph_request # each subgraph request updates a metric separately errors: # Only work if it's a valid GraphQL error (for example if the subgraph returns an http error or if the router can't reach the subgraph) include_messages: true # Will include the error message in a message attribute extensions: # Include extensions data