diff --git a/.changesets/feat_aguilarjaf_add_response_errors_selector.md b/.changesets/feat_aguilarjaf_add_response_errors_selector.md new file mode 100644 index 0000000000..0aefaa8f4b --- /dev/null +++ b/.changesets/feat_aguilarjaf_add_response_errors_selector.md @@ -0,0 +1,17 @@ +### add ResponseErrors selector to router response ([PR #7882](https://github.com/apollographql/router/pull/7882)) + +Introducing the `ResponseErrors` selector in telemetry configurations to capture router response errors, allowing users to capture and log errors encountered at the router service layer. This new selector enhances logging for the router service, as it allows users the option to only log router errors instead of the entire router response body to reduce noise. + +``` yaml +telemetry: + instrumentation: + events: + router: + router.error: + attributes: + "my_attribute": + response_errors: "$.[0]" + # Examples: "$.[0].message", "$.[0].locations", "$.[0].extensions", etc. +``` + +By [@Aguilarjaf](https://github.com/Aguilarjaf) in https://github.com/apollographql/router/pull/7882 \ 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 5aa80ac60c..542242d3d7 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 @@ -6056,6 +6056,20 @@ snapshot_kind: text ], "type": "object" }, + { + "additionalProperties": false, + "description": "The body response errors", + "properties": { + "response_errors": { + "description": "The router response body json path of the chunks.", + "type": "string" + } + }, + "required": [ + "response_errors" + ], + "type": "object" + }, { "additionalProperties": false, "description": "A header from the response", diff --git a/apollo-router/src/plugins/telemetry/config_new/router/selectors.rs b/apollo-router/src/plugins/telemetry/config_new/router/selectors.rs index 741f2a7f7d..fd328f6206 100644 --- a/apollo-router/src/plugins/telemetry/config_new/router/selectors.rs +++ b/apollo-router/src/plugins/telemetry/config_new/router/selectors.rs @@ -1,11 +1,15 @@ +use derivative::Derivative; use schemars::JsonSchema; use serde::Deserialize; +use serde_json::from_str; +use serde_json_bytes::path::JsonPathInst; use sha2::Digest; use super::events::DisplayRouterResponse; use crate::Context; use crate::context::CONTAINS_GRAPHQL_ERROR; use crate::context::OPERATION_NAME; +use crate::plugin::serde::deserialize_jsonpath; use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config::TraceIdFormat; use crate::plugins::telemetry::config_new::Selector; @@ -38,8 +42,9 @@ impl From<&RouterValue> for InstrumentValue { } } -#[derive(Deserialize, JsonSchema, Clone, Debug, PartialEq)] +#[derive(Derivative, Deserialize, JsonSchema, Clone)] #[serde(deny_unknown_fields, untagged)] +#[derivative(Debug, PartialEq)] pub(crate) enum RouterSelector { /// A value from baggage. Baggage { @@ -117,6 +122,14 @@ pub(crate) enum RouterSelector { /// The response body enabled or not response_body: bool, }, + /// The body response errors + ResponseErrors { + /// The router response body json path of the chunks. + #[schemars(with = "String")] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + #[serde(deserialize_with = "deserialize_jsonpath")] + response_errors: JsonPathInst, + }, /// A header from the response ResponseHeader { /// The name of the request header. @@ -168,6 +181,13 @@ impl Selector for RouterSelector { type EventResponse = (); fn on_request(&self, request: &router::Request) -> Option { + // Helper function to insert DisplayRouterResponse into request context extensions + fn insert_display_router_response(request: &router::Request) { + request.context.extensions().with_lock(|ext| { + ext.insert(DisplayRouterResponse); + }); + } + match self { RouterSelector::RequestMethod { request_method } if *request_method => { Some(request.router_request.method().to_string().into()) @@ -217,9 +237,11 @@ impl Selector for RouterSelector { RouterSelector::Static(val) => Some(val.clone().into()), RouterSelector::StaticField { r#static } => Some(r#static.clone().into()), RouterSelector::ResponseBody { response_body } if *response_body => { - request.context.extensions().with_lock(|ext| { - ext.insert(DisplayRouterResponse); - }); + insert_display_router_response(request); + None + } + RouterSelector::ResponseErrors { .. } => { + insert_display_router_response(request); None } // Related to Response @@ -239,6 +261,24 @@ impl Selector for RouterSelector { }) .map(|v| opentelemetry::Value::String(v.0.into())) } + RouterSelector::ResponseErrors { response_errors } => response + .context + .extensions() + .with_lock(|ext| ext.get::().cloned()) + .and_then(|v| { + from_str::(&v.0) + .ok() + .and_then(|body_json| { + let errors = body_json.get("errors"); + + let data: serde_json_bytes::Value = + serde_json_bytes::to_value(errors).ok()?; + + let val = response_errors.find(&data); + + val.maybe_to_otel_value() + }) + }), RouterSelector::ResponseHeader { response_header, default, @@ -418,6 +458,7 @@ mod test { use opentelemetry::trace::TraceId; use opentelemetry::trace::TraceState; use serde_json::json; + use serde_json_bytes::path::JsonPathInst; use tower::BoxError; use tracing::span; use tracing::subscriber; @@ -910,4 +951,27 @@ mod test { r#"{"data":"some data"}"# ); } + + #[test] + fn router_response_body_errors() { + let selector = RouterSelector::ResponseErrors { + response_errors: JsonPathInst::new("$.[0]").unwrap(), + }; + let res = &crate::services::RouterResponse::fake_builder() + .status_code(StatusCode::BAD_REQUEST) + .data("some data") + .errors(vec![ + crate::graphql::Error::builder() + .message("Something went wrong") + .locations(vec![crate::graphql::Location { line: 1, column: 1 }]) + .extension_code("GRAPHQL_VALIDATION_FAILED") + .build(), + ]) + .build() + .unwrap(); + assert_eq!( + selector.on_response(res).unwrap().as_str(), + r#"{"message":"Something went wrong","locations":[{"line":1,"column":1}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}"# + ); + } } diff --git a/docs/source/routing/observability/telemetry/instrumentation/selectors.mdx b/docs/source/routing/observability/telemetry/instrumentation/selectors.mdx index 4efdcc1eb9..127437ddc6 100644 --- a/docs/source/routing/observability/telemetry/instrumentation/selectors.mdx +++ b/docs/source/routing/observability/telemetry/instrumentation/selectors.mdx @@ -42,6 +42,8 @@ The router service is the initial entrypoint for all requests. It is HTTP centri | `response_header` | Yes | | The name of a response header | | `response_status` | Yes | `code` \| `reason` | The response status | | `response_context` | Yes | | The name of a response context key | +| `response_body` | Yes | | JSON Path into the router response body data (it might impact performance) | +| `response_errors` | Yes | | JSON Path into the router response body errors (it might impact performance) | | `baggage` | Yes | | The name of a baggage item | | `env` | Yes | | The name of an environment variable | | `on_graphql_error` | No | `true` \| `false` | Boolean set to true if the response payload contains a GraphQL error | @@ -194,6 +196,43 @@ telemetry: response_errors: "$.[0].extensions.code" ``` +### Capturing the Router Response body + +The `response_body` selector allows for accessing the response body in the telemetry +configurations, enabling more detailed monitoring and logging of response data in the Router. + +```yaml +telemetry: + instrumentation: + spans: + router: + attributes: + "my_attribute": + response_body: true +``` + +### Capturing Router Response Errors + +The `response_errors` selector allows for a more granular approach towards gathering specific error +data from the Router response with JSON paths, avoiding capturing the entire response body +while still providing relevant error information. + +```yaml +telemetry: + instrumentation: + events: + router: + router.error: + attributes: + "my_attribute": + response_errors: "$.[0]" + + # Other examples: + # response_errors: "$.[0].message" + # response_errors: "$.[0].locations" + # response_errors: "$.[0].extensions" +``` + ### Getting GraphQL operation info Configuring the `query` selector to get information about GraphQL operations, with an example for a custom view of operation limits: