diff --git a/.changesets/feat_rreg_additional_connector_instrument_selectors.md b/.changesets/feat_rreg_additional_connector_instrument_selectors.md new file mode 100644 index 0000000000..a00c65c7bb --- /dev/null +++ b/.changesets/feat_rreg_additional_connector_instrument_selectors.md @@ -0,0 +1,20 @@ +### Additional Connector Custom Instrument Selectors ([PR #8045](https://github.com/apollographql/router/pull/8045)) + +This adds new [custom instrument selectors](https://www.apollographql.com/docs/graphos/routing/observability/telemetry/instrumentation/selectors#connector) for Connectors and enhances some existing selectors. The new selectors are: + - `supergraph_operation_name` + - The supergraph's operation name + - `supergraph_operation_kind` + - The supergraph's operation type (e.g. `query`, `mutation`, `subscription`) + - `request_context` + - Takes the value of the given key on the request context + - `connector_on_response_error` + - Returns true when the response does not meet the `is_successful` condition. Or, if that condition is not set, + returns true when the response has a non-200 status code + +These selectors were modified to add additional functionality: + - `connector_request_mapping_problems` + - Adds a new `boolean` variant that will return `true` when a mapping problem exists on the request + - `connector_response_mapping_problems` + - Adds a new `boolean` variant that will return `true` when a mapping problem exists on the response + +By [@rregitsky](https://github.com/rregitsky) in https://github.com/apollographql/router/pull/8045 \ No newline at end of file diff --git a/.changesets/feat_rreg_experimental_subgraph_metrics.md b/.changesets/feat_rreg_experimental_subgraph_metrics.md index 481da985e6..592f8dd3e5 100644 --- a/.changesets/feat_rreg_experimental_subgraph_metrics.md +++ b/.changesets/feat_rreg_experimental_subgraph_metrics.md @@ -1,4 +1,4 @@ -### [Subgraph Insights] Experimental Apollo Subgraph Fetch Histogram ([PR #8013](https://github.com/apollographql/router/pull/8013)) +### [Subgraph Insights] Experimental Apollo Subgraph Fetch Histogram ([PR #8013](https://github.com/apollographql/router/pull/8013), [PR #8045](https://github.com/apollographql/router/pull/8045)) This change adds a new, experimental histogram to capture subgraph fetch duration for GraphOS. This will eventually be used to power subgraph-level insights in Apollo Studio. @@ -15,4 +15,4 @@ The new instrument is only sent to GraphOS and is not available in 3rd-party OTe customizable. Users requiring a customizable alternative can use the existing `http.client.request.duration` instrument, which measures the same value. -By [@rregitsky](https://github.com/rregitsky) in https://github.com/apollographql/router/pull/8013 \ No newline at end of file +By [@rregitsky](https://github.com/rregitsky) in https://github.com/apollographql/router/pull/8013 and https://github.com/apollographql/router/pull/8045 \ No newline at end of file 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 ec32c83768..38bda37c77 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 @@ -2175,6 +2175,68 @@ snapshot_kind: text "connector_response_mapping_problems" ], "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "default": { + "$ref": "#/definitions/AttributeValue", + "description": "#/definitions/AttributeValue", + "nullable": true + }, + "request_context": { + "description": "The request context key.", + "type": "string" + } + }, + "required": [ + "request_context" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "default": { + "description": "Optional default value.", + "nullable": true, + "type": "string" + }, + "supergraph_operation_name": { + "$ref": "#/definitions/OperationName", + "description": "#/definitions/OperationName" + } + }, + "required": [ + "supergraph_operation_name" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "supergraph_operation_kind": { + "$ref": "#/definitions/OperationKind", + "description": "#/definitions/OperationKind" + } + }, + "required": [ + "supergraph_operation_kind" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "connector_on_response_error": { + "description": "Boolean set to true if the response's `is_successful` condition is false. If this is not set, returns true when the response contains a non-200 status code", + "type": "boolean" + } + }, + "required": [ + "connector_on_response_error" + ], + "type": "object" } ] }, @@ -4400,11 +4462,29 @@ snapshot_kind: text "type": "object" }, "MappingProblems": { - "enum": [ - "problems", - "count" - ], - "type": "string" + "oneOf": [ + { + "description": "String representation of all problems", + "enum": [ + "problems" + ], + "type": "string" + }, + { + "description": "The count of mapping problems", + "enum": [ + "count" + ], + "type": "string" + }, + { + "description": "Whether there are any mapping problems", + "enum": [ + "boolean" + ], + "type": "string" + } + ] }, "MetricAggregation": { "oneOf": [ diff --git a/apollo-router/src/plugins/telemetry/config_new/apollo/instruments.rs b/apollo-router/src/plugins/telemetry/config_new/apollo/instruments.rs index 3b2d457e91..0be663c45c 100644 --- a/apollo-router/src/plugins/telemetry/config_new/apollo/instruments.rs +++ b/apollo-router/src/plugins/telemetry/config_new/apollo/instruments.rs @@ -17,6 +17,10 @@ use crate::plugins::telemetry::GRAPHQL_OPERATION_NAME_ATTRIBUTE; use crate::plugins::telemetry::GRAPHQL_OPERATION_TYPE_ATTRIBUTE; use crate::plugins::telemetry::apollo::Config; use crate::plugins::telemetry::config_new::attributes::StandardAttribute; +use crate::plugins::telemetry::config_new::connector::ConnectorRequest; +use crate::plugins::telemetry::config_new::connector::ConnectorResponse; +use crate::plugins::telemetry::config_new::connector::attributes::ConnectorAttributes; +use crate::plugins::telemetry::config_new::connector::selectors::ConnectorSelector; use crate::plugins::telemetry::config_new::extendable::Extendable; use crate::plugins::telemetry::config_new::instruments::APOLLO_ROUTER_OPERATIONS_FETCH_DURATION; use crate::plugins::telemetry::config_new::instruments::CustomHistogram; @@ -43,6 +47,18 @@ pub(crate) struct ApolloSubgraphInstruments { >, } +pub(crate) struct ApolloConnectorInstruments { + pub(crate) apollo_router_operations_fetch_duration: Option< + CustomHistogram< + ConnectorRequest, + ConnectorResponse, + (), + ConnectorAttributes, + ConnectorSelector, + >, + >, +} + impl ApolloSubgraphInstruments { pub(crate) fn new( static_instruments: Arc>, @@ -127,21 +143,7 @@ impl ApolloSubgraphInstruments { } pub(crate) fn new_builtin() -> HashMap { - let meter = metrics::meter_provider().meter(METER_NAME); - let mut static_instruments = HashMap::with_capacity(1); - - static_instruments.insert( - APOLLO_ROUTER_OPERATIONS_FETCH_DURATION.to_string(), - StaticInstrument::Histogram( - meter - .f64_histogram(APOLLO_ROUTER_OPERATIONS_FETCH_DURATION) - .with_unit("s") - .with_description("Duration of a subgraph fetch.") - .init(), - ), - ); - - static_instruments + create_subgraph_and_connector_shared_static_instruments() } } @@ -174,3 +176,137 @@ impl Instrumented for ApolloSubgraphInstruments { } } } + +impl ApolloConnectorInstruments { + pub(crate) fn new( + static_instruments: Arc>, + apollo_config: Config, + ) -> Self { + let selectors = Extendable { + attributes: ConnectorAttributes::builder() + .subgraph_name(StandardAttribute::Bool(true)) + .build(), + custom: HashMap::from([ + ( + APOLLO_CLIENT_NAME_ATTRIBUTE.to_string(), + ConnectorSelector::RequestContext { + request_context: CLIENT_NAME.to_string(), + redact: None, + default: None, + }, + ), + ( + APOLLO_CLIENT_VERSION_ATTRIBUTE.to_string(), + ConnectorSelector::RequestContext { + request_context: CLIENT_VERSION.to_string(), + redact: None, + default: None, + }, + ), + ( + GRAPHQL_OPERATION_NAME_ATTRIBUTE.to_string(), + ConnectorSelector::SupergraphOperationName { + supergraph_operation_name: OperationName::String, + redact: None, + default: None, + }, + ), + ( + GRAPHQL_OPERATION_TYPE_ATTRIBUTE.to_string(), + ConnectorSelector::SupergraphOperationKind { + supergraph_operation_kind: OperationKind::String, + }, + ), + ( + APOLLO_OPERATION_ID_ATTRIBUTE.to_string(), + ConnectorSelector::RequestContext { + request_context: APOLLO_OPERATION_ID.to_string(), + redact: None, + default: None, + }, + ), + ( + APOLLO_HAS_ERRORS_ATTRIBUTE.to_string(), + ConnectorSelector::OnResponseError { + connector_on_response_error: true, + }, + ), + ]), + }; + let attribute_count = selectors.custom.len() + 1; // 1 for subgraph_name on attributes + + let apollo_router_operations_fetch_duration = + apollo_config.experimental_subgraph_metrics.then(|| { + CustomHistogram::builder() + .increment(Increment::Duration(Instant::now())) + .attributes(Vec::with_capacity(attribute_count)) + .selectors(Arc::new(selectors)) + .histogram(static_instruments + .get(APOLLO_ROUTER_OPERATIONS_FETCH_DURATION) + .expect( + "cannot get apollo static instrument for subgraph; this should not happen", + ) + .as_histogram() + .cloned() + .expect( + "cannot convert apollo instrument to histogram for subgraph; this should not happen", + ) + ) + .build() + }); + + Self { + apollo_router_operations_fetch_duration, + } + } + + pub(crate) fn new_builtin() -> HashMap { + create_subgraph_and_connector_shared_static_instruments() + } +} + +impl Instrumented for ApolloConnectorInstruments { + type Request = ConnectorRequest; + type Response = ConnectorResponse; + type EventResponse = (); + + fn on_request(&self, request: &Self::Request) { + if let Some(apollo_router_operations_fetch_duration) = + &self.apollo_router_operations_fetch_duration + { + apollo_router_operations_fetch_duration.on_request(request); + } + } + + fn on_response(&self, response: &Self::Response) { + if let Some(apollo_router_operations_fetch_duration) = + &self.apollo_router_operations_fetch_duration + { + apollo_router_operations_fetch_duration.on_response(response); + } + } + + fn on_error(&self, error: &BoxError, ctx: &Context) { + if let Some(apollo_router_operations_fetch_duration) = + &self.apollo_router_operations_fetch_duration + { + apollo_router_operations_fetch_duration.on_error(error, ctx); + } + } +} + +fn create_subgraph_and_connector_shared_static_instruments() -> HashMap { + let meter = metrics::meter_provider().meter(METER_NAME); + let mut static_instruments = HashMap::with_capacity(1); + static_instruments.insert( + APOLLO_ROUTER_OPERATIONS_FETCH_DURATION.to_string(), + StaticInstrument::Histogram( + meter + .f64_histogram(APOLLO_ROUTER_OPERATIONS_FETCH_DURATION) + .with_unit("s") + .with_description("Duration of a subgraph fetch.") + .init(), + ), + ); + static_instruments +} diff --git a/apollo-router/src/plugins/telemetry/config_new/connector/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/connector/attributes.rs index 0e2b5e2707..4e61162a20 100644 --- a/apollo-router/src/plugins/telemetry/config_new/connector/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/connector/attributes.rs @@ -18,7 +18,7 @@ const CONNECTOR_HTTP_METHOD: Key = Key::from_static_str("connector.http.method") const CONNECTOR_SOURCE_NAME: Key = Key::from_static_str("connector.source.name"); const CONNECTOR_URL_TEMPLATE: Key = Key::from_static_str("connector.url.template"); -#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug, buildstructor::Builder)] #[serde(deny_unknown_fields, default)] pub(crate) struct ConnectorAttributes { /// The name of the subgraph containing the connector diff --git a/apollo-router/src/plugins/telemetry/config_new/connector/selectors.rs b/apollo-router/src/plugins/telemetry/config_new/connector/selectors.rs index 0afd128f9d..20b042969b 100644 --- a/apollo-router/src/plugins/telemetry/config_new/connector/selectors.rs +++ b/apollo-router/src/plugins/telemetry/config_new/connector/selectors.rs @@ -7,17 +7,23 @@ use opentelemetry::StringValue; use opentelemetry::Value; use schemars::JsonSchema; use serde::Deserialize; +use sha2::Digest; use tower::BoxError; use crate::Context; +use crate::context::OPERATION_KIND; +use crate::context::OPERATION_NAME; use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config_new::Selector; use crate::plugins::telemetry::config_new::Stage; +use crate::plugins::telemetry::config_new::ToOtelValue; use crate::plugins::telemetry::config_new::connector::ConnectorRequest; use crate::plugins::telemetry::config_new::connector::ConnectorResponse; use crate::plugins::telemetry::config_new::instruments::InstrumentValue; use crate::plugins::telemetry::config_new::instruments::Standard; use crate::plugins::telemetry::config_new::selectors::ErrorRepr; +use crate::plugins::telemetry::config_new::selectors::OperationKind; +use crate::plugins::telemetry::config_new::selectors::OperationName; use crate::plugins::telemetry::config_new::selectors::ResponseStatus; #[derive(Deserialize, JsonSchema, Clone, Debug, PartialEq)] @@ -46,8 +52,12 @@ impl From<&ConnectorValue> for InstrumentValue { #[derive(Deserialize, JsonSchema, Clone, Debug, PartialEq)] #[serde(deny_unknown_fields, rename_all = "snake_case")] pub(crate) enum MappingProblems { + /// String representation of all problems Problems, + /// The count of mapping problems Count, + /// Whether there are any mapping problems + Boolean, } #[derive(Deserialize, JsonSchema, Clone, Derivative)] @@ -110,6 +120,37 @@ pub(crate) enum ConnectorSelector { /// Response mapping problems, if any connector_response_mapping_problems: MappingProblems, }, + RequestContext { + /// The request context key. + request_context: String, + #[serde(skip)] + #[allow(dead_code)] + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + SupergraphOperationName { + /// The supergraph query operation name. + supergraph_operation_name: OperationName, + #[serde(skip)] + #[allow(dead_code)] + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + SupergraphOperationKind { + /// The supergraph query operation kind (query|mutation|subscription). + // Allow dead code is required because there is only one variant in OperationKind and we need to avoid the dead code warning. + #[allow(dead_code)] + supergraph_operation_kind: OperationKind, + }, + OnResponseError { + /// Boolean set to true if the response's `is_successful` condition is false. If this is not + /// set, returns true when the response contains a non-200 status code + connector_on_response_error: bool, + }, } impl Selector for ConnectorSelector { @@ -172,8 +213,42 @@ impl Selector for ConnectorSelector { .map(|problem| problem.count as i64) .sum(), )), + MappingProblems::Boolean => Some(Value::Bool(!request.mapping_problems.is_empty())), }, ConnectorSelector::StaticField { r#static } => Some(r#static.clone().into()), + ConnectorSelector::RequestContext { + request_context, + default, + .. + } => request + .context + .get_json_value(request_context) + .as_ref() + .and_then(|v| v.maybe_to_otel_value()) + .or_else(|| default.maybe_to_otel_value()), + ConnectorSelector::SupergraphOperationName { + supergraph_operation_name, + default, + .. + } => { + let op_name = request.context.get(OPERATION_NAME).ok().flatten(); + match supergraph_operation_name { + OperationName::String => op_name.or_else(|| default.clone()), + OperationName::Hash => op_name.or_else(|| default.clone()).map(|op_name| { + let mut hasher = sha2::Sha256::new(); + hasher.update(op_name.as_bytes()); + let result = hasher.finalize(); + hex::encode(result) + }), + } + .map(opentelemetry::Value::from) + } + ConnectorSelector::SupergraphOperationKind { .. } => request + .context + .get::<_, String>(OPERATION_KIND) + .ok() + .flatten() + .map(opentelemetry::Value::from), _ => None, } } @@ -228,16 +303,22 @@ impl Selector for ConnectorSelector { MappingProblems::Count => Some(Value::I64( problems.iter().map(|problem| problem.count as i64).sum(), )), + MappingProblems::Boolean => Some(Value::Bool(!problems.is_empty())), } } else { None } } + ConnectorSelector::OnResponseError { + connector_on_response_error, + } if *connector_on_response_error => { + Some(matches!(response.mapped_response, MappedResponse::Error { .. }).into()) + } _ => None, } } - fn on_error(&self, error: &BoxError, _: &Context) -> Option { + fn on_error(&self, error: &BoxError, _ctx: &Context) -> Option { match self { ConnectorSelector::Error { .. } => Some(error.to_string().into()), ConnectorSelector::StaticField { r#static } => Some(r#static.clone().into()), @@ -263,6 +344,9 @@ impl Selector for ConnectorSelector { | ConnectorSelector::ConnectorUrlTemplate { .. } | ConnectorSelector::StaticField { .. } | ConnectorSelector::RequestMappingProblems { .. } + | ConnectorSelector::RequestContext { .. } + | ConnectorSelector::SupergraphOperationName { .. } + | ConnectorSelector::SupergraphOperationKind { .. } ), Stage::Response => matches!( self, @@ -274,6 +358,7 @@ impl Selector for ConnectorSelector { | ConnectorSelector::ConnectorUrlTemplate { .. } | ConnectorSelector::StaticField { .. } | ConnectorSelector::ResponseMappingProblems { .. } + | ConnectorSelector::OnResponseError { .. } ), Stage::ResponseEvent => false, Stage::ResponseField => false, @@ -305,6 +390,7 @@ mod tests { use apollo_federation::connectors::ProblemLocation; use apollo_federation::connectors::SourceName; use apollo_federation::connectors::StringTemplate; + use apollo_federation::connectors::runtime::errors::RuntimeError; use apollo_federation::connectors::runtime::http_json_transport::HttpRequest; use apollo_federation::connectors::runtime::http_json_transport::HttpResponse; use apollo_federation::connectors::runtime::http_json_transport::TransportRequest; @@ -322,7 +408,12 @@ mod tests { use super::ConnectorSource; use super::MappingProblems; use crate::Context; + use crate::context::OPERATION_KIND; + use crate::context::OPERATION_NAME; use crate::plugins::telemetry::config_new::Selector; + use crate::plugins::telemetry::config_new::selectors::ErrorRepr; + use crate::plugins::telemetry::config_new::selectors::OperationKind; + use crate::plugins::telemetry::config_new::selectors::OperationName; use crate::plugins::telemetry::config_new::selectors::ResponseStatus; use crate::services::connector::request_service::Request; use crate::services::connector::request_service::Response; @@ -335,10 +426,6 @@ mod tests { const TEST_HEADER_VALUE: &str = "test_header_value"; const TEST_STATIC: &str = "test_static"; - fn context() -> Context { - Context::default() - } - fn connector() -> Connector { Connector { id: ConnectId::new( @@ -390,23 +477,20 @@ mod tests { http_request } - fn connector_request(http_request: http::Request) -> Request { - connector_request_with_mapping_problems(http_request, vec![]) - } - - fn connector_request_with_mapping_problems( + fn connector_request( http_request: http::Request, - mapping_problems: Vec, + context: Option, + mapping_problems: Option>, ) -> Request { Request { - context: context(), + context: context.unwrap_or_default(), connector: Arc::new(connector()), transport_request: TransportRequest::Http(HttpRequest { inner: http_request, debug: Default::default(), }), key: response_key(), - mapping_problems, + mapping_problems: mapping_problems.unwrap_or_default(), supergraph_request: Default::default(), } } @@ -438,6 +522,24 @@ mod tests { } } + fn connector_response_with_mapped_error(status_code: StatusCode) -> Response { + Response { + transport_result: Ok(TransportResponse::Http(HttpResponse { + inner: http::Response::builder() + .status(status_code) + .body(body::empty()) + .expect("expecting valid response") + .into_parts() + .0, + })), + mapped_response: MappedResponse::Error { + error: RuntimeError::new("Internal server errror", &response_key()), + key: response_key(), + problems: vec![], + }, + } + } + fn connector_response_with_header() -> Response { Response { transport_result: Ok(TransportResponse::Http(HttpResponse { @@ -503,7 +605,7 @@ mod tests { }; assert_eq!( Some(TEST_STATIC.into()), - selector.on_request(&connector_request(http_request())) + selector.on_request(&connector_request(http_request(), None, None)) ); } @@ -514,7 +616,7 @@ mod tests { }; assert_eq!( Some(TEST_SUBGRAPH_NAME.into()), - selector.on_request(&connector_request(http_request())) + selector.on_request(&connector_request(http_request(), None, None)) ); } @@ -525,7 +627,7 @@ mod tests { }; assert_eq!( Some(TEST_SOURCE_NAME.into()), - selector.on_request(&connector_request(http_request())) + selector.on_request(&connector_request(http_request(), None, None)) ); } @@ -536,7 +638,7 @@ mod tests { }; assert_eq!( Some(TEST_URL_TEMPLATE.into()), - selector.on_request(&connector_request(http_request())) + selector.on_request(&connector_request(http_request(), None, None)) ); } @@ -549,7 +651,7 @@ mod tests { }; assert_eq!( Some("defaulted".into()), - selector.on_request(&connector_request(http_request())) + selector.on_request(&connector_request(http_request(), None, None)) ); } @@ -562,7 +664,7 @@ mod tests { }; assert_eq!( Some(TEST_HEADER_VALUE.into()), - selector.on_request(&connector_request(http_request_with_header())) + selector.on_request(&connector_request(http_request_with_header(), None, None)) ); } @@ -632,7 +734,7 @@ mod tests { }; assert_eq!( Some(Value::Array(Array::String(vec![]))), - selector.on_request(&connector_request(http_request())) + selector.on_request(&connector_request(http_request(), None, None)) ); } @@ -643,7 +745,7 @@ mod tests { }; assert_eq!( Some(0.into()), - selector.on_request(&connector_request(http_request())) + selector.on_request(&connector_request(http_request(), None, None)) ); } @@ -654,9 +756,10 @@ mod tests { }; assert_eq!( Some(mapping_problem_array()), - selector.on_request(&connector_request_with_mapping_problems( + selector.on_request(&connector_request( http_request(), - mapping_problems() + None, + Some(mapping_problems()) )) ); } @@ -668,9 +771,25 @@ mod tests { }; assert_eq!( Some(6.into()), - selector.on_request(&connector_request_with_mapping_problems( + selector.on_request(&connector_request( http_request(), - mapping_problems() + None, + Some(mapping_problems()), + )) + ); + } + + #[test] + fn connector_on_request_mapping_problems_boolean() { + let selector = ConnectorSelector::RequestMappingProblems { + connector_request_mapping_problems: MappingProblems::Boolean, + }; + assert_eq!( + Some(true.into()), + selector.on_request(&connector_request( + http_request(), + None, + Some(mapping_problems()), )) ); } @@ -725,6 +844,20 @@ mod tests { ); } + #[test] + fn connector_on_response_mapping_problems_boolean() { + let selector = ConnectorSelector::ResponseMappingProblems { + connector_response_mapping_problems: MappingProblems::Boolean, + }; + assert_eq!( + Some(true.into()), + selector.on_response(&connector_response_with_mapping_problems( + StatusCode::OK, + mapping_problems() + )) + ); + } + #[test] fn connector_on_drop_static_field() { let selector = ConnectorSelector::StaticField { @@ -732,4 +865,115 @@ mod tests { }; assert_eq!(Some(TEST_STATIC.into()), selector.on_drop()); } + + #[test] + fn connector_request_context() { + let selector = ConnectorSelector::RequestContext { + request_context: "context_key".to_string(), + redact: None, + default: Some("defaulted".into()), + }; + let context = Context::new(); + let _ = context.insert("context_key".to_string(), "context_value".to_string()); + assert_eq!( + selector + .on_request(&connector_request(http_request(), Some(context), None)) + .unwrap(), + "context_value".into() + ); + + assert_eq!( + selector + .on_request(&connector_request(http_request(), None, None)) + .unwrap(), + "defaulted".into() + ); + } + + #[test] + fn connector_supergraph_operation_name_string() { + let selector = ConnectorSelector::SupergraphOperationName { + supergraph_operation_name: OperationName::String, + redact: None, + default: Some("defaulted".to_string()), + }; + let context = Context::new(); + let _ = context.insert(OPERATION_NAME, "topProducts".to_string()); + + assert_eq!( + selector.on_request(&connector_request(http_request(), None, None)), + Some("defaulted".into()) + ); + assert_eq!( + selector.on_request(&connector_request(http_request(), Some(context), None)), + Some("topProducts".into()) + ); + } + + #[test] + fn connector_supergraph_operation_name_hash() { + let selector = ConnectorSelector::SupergraphOperationName { + supergraph_operation_name: OperationName::Hash, + redact: None, + default: Some("defaulted".to_string()), + }; + let context = Context::new(); + let _ = context.insert(OPERATION_NAME, "topProducts".to_string()); + assert_eq!( + selector.on_request(&connector_request(http_request(), None, None)), + Some("96294f50edb8f006f6b0a2dadae50d3c521e9841d07d6395d91060c8ccfed7f0".into()) + ); + + assert_eq!( + selector.on_request(&connector_request(http_request(), Some(context), None)), + Some("bd141fca26094be97c30afd42e9fc84755b252e7052d8c992358319246bd555a".into()) + ); + } + + #[test] + fn connector_supergraph_operation_kind() { + let selector = ConnectorSelector::SupergraphOperationKind { + supergraph_operation_kind: OperationKind::String, + }; + let context = Context::new(); + let _ = context.insert(OPERATION_KIND, "query".to_string()); + assert_eq!( + selector.on_request(&connector_request(http_request(), Some(context), None)), + Some("query".into()) + ); + } + + #[test] + fn connector_on_response_error() { + let selector = ConnectorSelector::OnResponseError { + connector_on_response_error: true, + }; + assert_eq!( + selector + .on_response(&connector_response_with_mapped_error( + StatusCode::INTERNAL_SERVER_ERROR + )) + .unwrap(), + Value::Bool(true) + ); + + assert_eq!( + selector + .on_response(&connector_response(StatusCode::OK)) + .unwrap(), + Value::Bool(false) + ); + } + + #[test] + fn error_reason() { + let selector = ConnectorSelector::Error { + error: ErrorRepr::Reason, + }; + let err = "NaN".parse::().unwrap_err(); + assert_eq!( + selector.on_error(&err.into(), &Context::new()).unwrap(), + Value::String("invalid digit found in string".into()) + ); + } } diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/apollo/connector_fetch_duration/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/apollo/connector_fetch_duration/metrics.snap new file mode 100644 index 0000000000..1b4573aa39 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/apollo/connector_fetch_duration/metrics.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: Apollo subgraph fetch duration histogram +expression: "&metrics.all()" +info: + telemetry: + apollo: + experimental_subgraph_metrics: true + instrumentation: + instruments: + connector: + http.client.request.duration: false +snapshot_kind: text +--- +- name: apollo.router.operations.fetch.duration + description: Duration of a subgraph fetch. + unit: s + data: + datapoints: + - sum: 0.1 + count: 1 + attributes: + apollo.client.name: myClient + apollo.client.version: v0.1.0 + apollo.operation.id: myOperationID + graphql.operation.name: Test + graphql.operation.type: query + has_errors: false + subgraph.name: posts diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/apollo/connector_fetch_duration/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/apollo/connector_fetch_duration/router.yaml new file mode 100644 index 0000000000..aac863bcc5 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/apollo/connector_fetch_duration/router.yaml @@ -0,0 +1,7 @@ +telemetry: + apollo: + experimental_subgraph_metrics: true + instrumentation: + instruments: + connector: + http.client.request.duration: false \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/apollo/connector_fetch_duration/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/apollo/connector_fetch_duration/test.yaml new file mode 100644 index 0000000000..a91c223e9a --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/apollo/connector_fetch_duration/test.yaml @@ -0,0 +1,21 @@ +description: Apollo subgraph fetch duration histogram +events: + - - context: + map: + "apollo::supergraph::operation_name": "Test" + "apollo::supergraph::operation_id": "myOperationID" + "apollo::supergraph::operation_kind": "query" + "apollo::telemetry::client_name": "myClient" + "apollo::telemetry::client_version": "v0.1.0" + - connector_request: + subgraph_name: posts + source_name: posts_api + http_method: GET + url_template: "/posts" + uri: "/posts" + - connector_response: + status: 200 + body: | + { "foo": "bar" } + headers: + custom_response_header: custom_response_header_value diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/http_client_request_duration/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/http_client_request_duration/metrics.snap index db1d610440..057e879a3f 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/http_client_request_duration/metrics.snap +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/http_client_request_duration/metrics.snap @@ -17,12 +17,21 @@ info: connector.url.template: true custom.request.header.attribute: connector_http_request_header: custom_request_header + custom.request.context: + request_context: "custom::context::key" custom.response.header.attribute: connector_http_response_header: custom_response_header custom.response.status.attribute: connector_http_response_status: code custom.static.attribute: static: custom_value + custom.supergraph.operation.name: + supergraph_operation_name: string + custom.supergraph.operation.kind: + supergraph_operation_kind: string + custom.has_error: + error: boolean +snapshot_kind: text --- - name: http.client.request.duration description: Duration of HTTP client requests. @@ -35,8 +44,12 @@ info: connector.http.method: GET connector.source: posts_api connector.url.template: /posts + custom.has_error: false + custom.request.context: custom_context_value custom.request.header.attribute: custom_request_header_value custom.response.header.attribute: custom_response_header_value custom.response.status.attribute: 200 custom.static.attribute: custom_value + custom.supergraph.operation.kind: query + custom.supergraph.operation.name: Test subgraph.name: posts diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/http_client_request_duration/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/http_client_request_duration/router.yaml index 276dfe9e6d..f092715928 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/http_client_request_duration/router.yaml +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/http_client_request_duration/router.yaml @@ -12,9 +12,17 @@ telemetry: connector.url.template: true custom.request.header.attribute: connector_http_request_header: "custom_request_header" + custom.request.context: + request_context: "custom::context::key" custom.response.header.attribute: connector_http_response_header: "custom_response_header" custom.response.status.attribute: connector_http_response_status: code custom.static.attribute: - static: "custom_value" \ No newline at end of file + static: "custom_value" + custom.supergraph.operation.name: + supergraph_operation_name: string + custom.supergraph.operation.kind: + supergraph_operation_kind: string + custom.has_error: + connector_on_response_error: true \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/http_client_request_duration/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/http_client_request_duration/test.yaml index 9f7c98b44e..b2fd3f8d18 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/http_client_request_duration/test.yaml +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/http_client_request_duration/test.yaml @@ -1,6 +1,11 @@ description: Connector HTTP client duration metric events: - - - connector_request: + - - context: + map: + "custom::context::key": "custom_context_value" + "apollo::supergraph::operation_name": "Test" + "apollo::supergraph::operation_kind": "query" + - connector_request: subgraph_name: posts source_name: posts_api http_method: GET diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/mapping_problems/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/mapping_problems/metrics.snap index 23c079705b..1efce9ccb4 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/mapping_problems/metrics.snap +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/mapping_problems/metrics.snap @@ -15,6 +15,8 @@ info: attributes: connector.source: connector_source: name + mapping.problems.exist: + connector_request_mapping_problems: boolean unit: count type: counter response.mapping.problems: @@ -24,8 +26,11 @@ info: attributes: connector.source: connector_source: name + mapping.problems.exist: + connector_response_mapping_problems: boolean unit: count type: counter +snapshot_kind: text --- - name: request.mapping.problems description: Count of connectors request mapping problems @@ -35,6 +40,7 @@ info: - value: 15 attributes: connector.source: user_api + mapping.problems.exist: true - name: response.mapping.problems description: Count of connectors response mapping problems unit: count @@ -43,3 +49,4 @@ info: - value: 5 attributes: connector.source: user_api + mapping.problems.exist: true diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/mapping_problems/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/mapping_problems/router.yaml index 4404da4815..492cc5b504 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/mapping_problems/router.yaml +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/connector/mapping_problems/router.yaml @@ -10,6 +10,8 @@ telemetry: attributes: connector.source: connector_source: name + mapping.problems.exist: + connector_request_mapping_problems: boolean unit: count type: counter response.mapping.problems: @@ -19,5 +21,8 @@ telemetry: attributes: connector.source: connector_source: name + mapping.problems.exist: + connector_response_mapping_problems: boolean unit: count - type: counter \ No newline at end of file + type: counter + diff --git a/apollo-router/src/plugins/telemetry/config_new/instruments.rs b/apollo-router/src/plugins/telemetry/config_new/instruments.rs index 99e00f75b3..eafe8bc93e 100644 --- a/apollo-router/src/plugins/telemetry/config_new/instruments.rs +++ b/apollo-router/src/plugins/telemetry/config_new/instruments.rs @@ -45,6 +45,7 @@ use crate::metrics; use crate::metrics::meter_provider; use crate::plugins::telemetry::apollo::Config; use crate::plugins::telemetry::config_new::Selectors; +use crate::plugins::telemetry::config_new::apollo::instruments::ApolloConnectorInstruments; use crate::plugins::telemetry::config_new::apollo::instruments::ApolloSubgraphInstruments; use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel; use crate::plugins::telemetry::config_new::conditions::Condition; @@ -778,6 +779,20 @@ impl InstrumentsConfig { static_instruments } + pub(crate) fn new_builtin_apollo_connector_instruments( + &self, + ) -> HashMap { + ApolloConnectorInstruments::new_builtin() + } + + pub(crate) fn new_apollo_connector_instruments( + &self, + static_instruments: Arc>, + apollo_config: Config, + ) -> ApolloConnectorInstruments { + ApolloConnectorInstruments::new(static_instruments, apollo_config) + } + pub(crate) fn new_builtin_graphql_instruments(&self) -> HashMap { let meter = metrics::meter_provider().meter(METER_NAME); let mut static_instruments = HashMap::with_capacity(self.graphql.custom.len()); @@ -2860,6 +2875,7 @@ mod tests { let mut subgraph_instruments = None; let mut connector_instruments = None; let mut apollo_subgraph_instruments = None; + let mut apollo_connector_instruments = None; let mut cache_instruments: Option = None; let graphql_instruments: GraphQLInstruments = config .new_graphql_instruments(Arc::new( @@ -3170,7 +3186,7 @@ mod tests { ), }; let request = Request { - context: Context::default(), + context: context.clone(), connector: Arc::new(connector), transport_request, key: response_key.clone(), @@ -3185,6 +3201,15 @@ mod tests { connector_instruments.on_request(&request); connector_instruments }); + apollo_connector_instruments = Some({ + let apollo_connector_instruments = config + .new_apollo_connector_instruments( + Arc::new(config.new_builtin_apollo_connector_instruments()), + apollo_config.clone(), + ); + apollo_connector_instruments.on_request(&request); + apollo_connector_instruments + }) } Event::ConnectorResponse { status, @@ -3223,6 +3248,10 @@ mod tests { .take() .expect("connector request must have been made first") .on_response(&response); + apollo_connector_instruments + .take() + .expect("connector request must have been made first") + .on_response(&response); } } } diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index e430203c71..cd2e395116 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -103,6 +103,7 @@ use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config::MetricsCommon; use crate::plugins::telemetry::config::TracingCommon; use crate::plugins::telemetry::config_new::DatadogId; +use crate::plugins::telemetry::config_new::apollo::instruments::ApolloConnectorInstruments; use crate::plugins::telemetry::config_new::apollo::instruments::ApolloSubgraphInstruments; use crate::plugins::telemetry::config_new::connector::events::ConnectorEvents; use crate::plugins::telemetry::config_new::cost::add_cost_attributes; @@ -321,6 +322,7 @@ struct BuiltinInstruments { subgraph_custom_instruments: Arc>, apollo_subgraph_instruments: Arc>, connector_custom_instruments: Arc>, + apollo_connector_instruments: Arc>, cache_custom_instruments: Arc>, _pipeline_instruments: Arc>, } @@ -333,6 +335,7 @@ fn create_builtin_instruments(config: &InstrumentsConfig) -> BuiltinInstruments subgraph_custom_instruments: Arc::new(config.new_builtin_subgraph_instruments()), apollo_subgraph_instruments: Arc::new(config.new_builtin_apollo_subgraph_instruments()), connector_custom_instruments: Arc::new(config.new_builtin_connector_instruments()), + apollo_connector_instruments: Arc::new(config.new_builtin_apollo_connector_instruments()), cache_custom_instruments: Arc::new(config.new_builtin_cache_instruments()), _pipeline_instruments: Arc::new(config.new_pipeline_instruments()), } @@ -1052,6 +1055,11 @@ impl PluginPrivate for Telemetry { .read() .connector_custom_instruments .clone(); + let static_apollo_connector_instruments = self + .builtin_instruments + .read() + .apollo_connector_instruments + .clone(); ServiceBuilder::new() .instrument(move |_req: &connector::request_service::Request| { span_mode.create_connector(source_name.as_str()) @@ -1063,6 +1071,14 @@ impl PluginPrivate for Telemetry { .instruments .new_connector_instruments(static_connector_instruments.clone()); custom_instruments.on_request(request); + let apollo_instruments = req_fn_config + .instrumentation + .instruments + .new_apollo_connector_instruments( + static_apollo_connector_instruments.clone(), + req_fn_config.apollo.clone(), + ); + apollo_instruments.on_request(request); let mut custom_events = req_fn_config.instrumentation.events.new_connector_events(); custom_events.on_request(request); @@ -1076,12 +1092,24 @@ impl PluginPrivate for Telemetry { ( request.context.clone(), - Some((custom_instruments, custom_events, custom_span_attributes)), + custom_instruments, + apollo_instruments, + custom_events, + custom_span_attributes, ) }, - move |(context, custom_telemetry): ( + move |( + context, + custom_instruments, + apollo_connector_instruments, + mut custom_events, + custom_span_attributes, + ): ( Context, - Option<(ConnectorInstruments, ConnectorEvents, Vec)>, + ConnectorInstruments, + ApolloConnectorInstruments, + ConnectorEvents, + Vec, ), f: BoxFuture< 'static, @@ -1089,44 +1117,37 @@ impl PluginPrivate for Telemetry { >| { let conf = res_fn_config.clone(); async move { - match custom_telemetry { - Some(( - custom_instruments, - mut custom_events, - custom_span_attributes, - )) => { - let span = Span::current(); - span.set_span_dyn_attributes(custom_span_attributes); - - let result = f.await; - match &result { - Ok(response) => { - span.set_span_dyn_attributes( - conf.instrumentation - .spans - .connector - .attributes - .on_response(response), - ); - custom_instruments.on_response(response); - custom_events.on_response(response); - } - Err(err) => { - span.set_span_dyn_attributes( - conf.instrumentation - .spans - .connector - .attributes - .on_error(err, &context), - ); - custom_instruments.on_error(err, &context); - custom_events.on_error(err, &context); - } - } - result + let span = Span::current(); + span.set_span_dyn_attributes(custom_span_attributes); + + let result = f.await; + match &result { + Ok(response) => { + span.set_span_dyn_attributes( + conf.instrumentation + .spans + .connector + .attributes + .on_response(response), + ); + custom_instruments.on_response(response); + apollo_connector_instruments.on_response(response); + custom_events.on_response(response); + } + Err(err) => { + span.set_span_dyn_attributes( + conf.instrumentation + .spans + .connector + .attributes + .on_error(err, &context), + ); + custom_instruments.on_error(err, &context); + apollo_connector_instruments.on_error(err, &context); + custom_events.on_error(err, &context); } - _ => f.await, } + result } }, ) diff --git a/apollo-router/tests/integration/telemetry/apollo_otel_metrics.rs b/apollo-router/tests/integration/telemetry/apollo_otel_metrics.rs index 8030237e4a..b06e4cc8aa 100644 --- a/apollo-router/tests/integration/telemetry/apollo_otel_metrics.rs +++ b/apollo-router/tests/integration/telemetry/apollo_otel_metrics.rs @@ -1,6 +1,7 @@ use std::fmt; use std::fmt::Debug; use std::fmt::Display; +use std::path::PathBuf; use std::time::Duration; use ahash::HashMap; @@ -554,6 +555,217 @@ async fn test_subgraph_request_emits_histogram() { router.graceful_shutdown().await; } +#[tokio::test(flavor = "multi_thread")] +async fn test_failed_subgraph_request_emits_histogram() { + if !graph_os_enabled() { + return; + } + let expected_operation_name = "ExampleQuery"; + let expected_client_name = "myClient"; + let expected_client_version = "v0.14"; + let expected_service = "products"; + let expected_operation_type = "query"; + + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Otlp { endpoint: None }) + .config( + r#" + telemetry: + apollo: + experimental_otlp_metrics_protocol: http + batch_processor: + scheduled_delay: 10ms + experimental_subgraph_metrics: true + include_subgraph_errors: + all: true + "#, + ) + .responder(ResponseTemplate::new(500).append_header("Content-Type", "application/json")) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let (_trace_id, _response) = router + .execute_query( + Query::builder() + .header("apollographql-client-name", expected_client_name) + .header("apollographql-client-version", expected_client_version) + .build(), + ) + .await; + + let metrics = router + .wait_for_emitted_otel_metrics(Duration::from_millis(20)) + .await; + assert!(!metrics.is_empty()); + assert_metrics_contain( + &metrics, + Metric::builder() + .name("apollo.router.operations.fetch.duration".to_string()) + .attribute("graphql.operation.name", expected_operation_name) + .attribute("apollo.client.name", expected_client_name) + .attribute("apollo.client.version", expected_client_version) + .attribute("subgraph.name", expected_service) + .attribute("graphql.operation.type", expected_operation_type) + .attribute("has_errors", true) + .count(1) + .build(), + ); + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_connector_request_emits_histogram() { + if !graph_os_enabled() { + return; + } + let expected_operation_name = "ExampleQuery"; + let expected_client_name = "myClient"; + let expected_client_version = "v0.14"; + let expected_service = "connectors"; + let expected_operation_type = "query"; + + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Otlp { endpoint: None }) + .config( + r#" + telemetry: + apollo: + experimental_otlp_metrics_protocol: http + batch_processor: + scheduled_delay: 10ms + experimental_subgraph_metrics: true + include_subgraph_errors: + all: true + "#, + ) + .supergraph(PathBuf::from_iter([ + "tests", + "fixtures", + "connectors", + "quickstart.graphql", + ])) + .responder(ResponseTemplate::new(200).set_body_json(json!([{ + "id": 1, + "title": "Awesome post", + "body:": "This is a really great post", + "userId": 1 + }]))) + .http_method("GET") + .build() + .await; + + router.start().await; + router.assert_started().await; + + let (_trace_id, _response) = router + .execute_query( + Query::builder() + .header("apollographql-client-name", expected_client_name) + .header("apollographql-client-version", expected_client_version) + .body(json!({"query":"query ExampleQuery {posts{id}}","variables":{}})) + .build(), + ) + .await; + + let metrics = router + .wait_for_emitted_otel_metrics(Duration::from_millis(20)) + .await; + assert!(!metrics.is_empty()); + assert_metrics_contain( + &metrics, + Metric::builder() + .name("apollo.router.operations.fetch.duration".to_string()) + .attribute("graphql.operation.name", expected_operation_name) + .attribute("apollo.client.name", expected_client_name) + .attribute("apollo.client.version", expected_client_version) + .attribute("subgraph.name", expected_service) + .attribute("graphql.operation.type", expected_operation_type) + .attribute("has_errors", false) + .count(1) + .build(), + ); + router.graceful_shutdown().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_failed_connector_request_emits_histogram() { + if !graph_os_enabled() { + return; + } + let expected_operation_name = "ExampleQuery"; + let expected_client_name = "myClient"; + let expected_client_version = "v0.14"; + let expected_service = "connectors"; + let expected_operation_type = "query"; + + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Otlp { endpoint: None }) + .config( + r#" + telemetry: + apollo: + experimental_otlp_metrics_protocol: http + batch_processor: + scheduled_delay: 10ms + experimental_subgraph_metrics: true + traffic_shaping: + connector: + sources: + connectors.jsonPlaceholder: + timeout: 1ns + include_subgraph_errors: + all: true + "#, + ) + .supergraph(PathBuf::from_iter([ + "..", + "apollo-router", + "tests", + "fixtures", + "connectors", + "quickstart.graphql", + ])) + .responder(ResponseTemplate::new(500).set_delay(Duration::from_millis(5))) + .http_method("GET") + .build() + .await; + + router.start().await; + router.assert_started().await; + + let (_trace_id, _response) = router + .execute_query( + Query::builder() + .header("apollographql-client-name", expected_client_name) + .header("apollographql-client-version", expected_client_version) + .body(json!({"query":"query ExampleQuery {posts{id}}","variables":{}})) + .build(), + ) + .await; + + let metrics = router + .wait_for_emitted_otel_metrics(Duration::from_millis(20)) + .await; + assert!(!metrics.is_empty()); + assert_metrics_contain( + &metrics, + Metric::builder() + .name("apollo.router.operations.fetch.duration".to_string()) + .attribute("graphql.operation.name", expected_operation_name) + .attribute("apollo.client.name", expected_client_name) + .attribute("apollo.client.version", expected_client_version) + .attribute("subgraph.name", expected_service) + .attribute("graphql.operation.type", expected_operation_type) + .attribute("has_errors", true) + .count(1) + .build(), + ); + router.graceful_shutdown().await; +} + /// Assert that the given metric exists in the list of Otel requests. This is a crude attempt at /// replicating _some_ assert_counter!() functionality since that test util can't be accessed here. fn assert_metrics_contain(actual_metrics: &[ExportMetricsServiceRequest], expected_metric: Metric) { diff --git a/docs/source/routing/observability/telemetry/instrumentation/selectors.mdx b/docs/source/routing/observability/telemetry/instrumentation/selectors.mdx index cfe50dc9a0..4efdcc1eb9 100644 --- a/docs/source/routing/observability/telemetry/instrumentation/selectors.mdx +++ b/docs/source/routing/observability/telemetry/instrumentation/selectors.mdx @@ -108,19 +108,23 @@ The subgraph service executes multiple times during query execution, with each e Apollo Connectors for REST APIs make HTTP calls to the upstream HTTP API. These selectors let you extract metrics from these HTTP requests and responses. -| Selector | Defaultable | Values | Description | -|---------------------------------------|-------------|---------------------|-------------------------------------------------------------------| -| `subgraph_name` | No | `true`\|`false` | The name of the subgraph containing the connector | -| `connector_source ` | No | `name` | The name of the `@source` associated with this connector, if any | -| `connector_http_request_header` | Yes | | The name of a connector request header | -| `connector_http_response_header` | Yes | | The name of a connector response header | -| `connector_http_response_status` | No | `code`\|`reason` | The status of a connector response | -| `connector_http_method` | No | `true`\|`false` | The HTTP method of a connector request | -| `connector_url_template ` | No | `true`\|`false` | The URL template of a connector request | -| `connector_request_mapping_problems` | No | `problems`\|`count` | Any mapping problems with the connector request | -| `connector_response_mapping_problems` | No | `problems`\|`count` | Any mapping problems with the connector response | -| `static` | No | | A static string value | -| `error` | No | `reason` | A string value containing error reason when it's a critical error | +| Selector | Defaultable | Values | Description | +|---------------------------------------|-------------|--------------------------------|--------------------------------------------------------------------------------------------------------------| +| `subgraph_name` | No | `true`\|`false` | The name of the subgraph containing the connector | +| `connector_source ` | No | `name` | The name of the `@source` associated with this connector, if any | +| `connector_http_request_header` | Yes | | The name of a connector request header | +| `connector_http_response_header` | Yes | | The name of a connector response header | +| `connector_http_response_status` | No | `code`\|`reason` | The status of a connector response | +| `connector_http_method` | No | `true`\|`false` | The HTTP method of a connector request | +| `connector_url_template ` | No | `true`\|`false` | The URL template of a connector request | +| `connector_request_mapping_problems` | No | `problems`\|`count`\|`boolean` | Any mapping problems with the connector request | +| `connector_response_mapping_problems` | No | `problems`\|`count`\|`boolean` | Any mapping problems with the connector response | +| `connector_on_response_error` | No | | Returns true if `is_successful` condition is false, or, if unset, if the response has a non-200 status code. | +| `static` | No | | A static string value | +| `error` | No | `reason` | A string value containing error reason when it's a critical error | +| `request_context` | Yes | | The value of a request context key | +| `supergraph_operation_name` | Yes | `string`\|`hash` | The operation name from the supergraph query | +| `supergraph_operation_kind` | Yes | `string` | The operation kind from the supergraph query | ### GraphQL