From 3cfe0520add5f1b4e8d9b8f71a767ec57a4d2d00 Mon Sep 17 00:00:00 2001 From: Aguilarjaf Date: Fri, 11 Jul 2025 11:59:03 -0700 Subject: [PATCH 1/6] feat: add truncate field to response body --- .../telemetry/config_new/router/selectors.rs | 62 +++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) 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..275ffcb072 100644 --- a/apollo-router/src/plugins/telemetry/config_new/router/selectors.rs +++ b/apollo-router/src/plugins/telemetry/config_new/router/selectors.rs @@ -1,5 +1,6 @@ use schemars::JsonSchema; use serde::Deserialize; +use serde_json::{from_str, to_string}; use sha2::Digest; use super::events::DisplayRouterResponse; @@ -22,6 +23,13 @@ use crate::plugins::telemetry::config_new::trace_id; use crate::query_planner::APOLLO_OPERATION_ID; use crate::services::router; +#[derive(Deserialize, JsonSchema, Clone, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +pub(crate) enum ResponseBodyField { + Data, + Errors, +} + #[derive(Deserialize, JsonSchema, Clone, Debug)] #[serde(deny_unknown_fields, rename_all = "snake_case", untagged)] pub(crate) enum RouterValue { @@ -116,6 +124,8 @@ pub(crate) enum RouterSelector { ResponseBody { /// The response body enabled or not response_body: bool, + /// Option to truncate the response body + truncate: Option, }, /// A header from the response ResponseHeader { @@ -216,7 +226,7 @@ impl Selector for RouterSelector { } => get_baggage(baggage).or_else(|| default.maybe_to_otel_value()), RouterSelector::Static(val) => Some(val.clone().into()), RouterSelector::StaticField { r#static } => Some(r#static.clone().into()), - RouterSelector::ResponseBody { response_body } if *response_body => { + RouterSelector::ResponseBody { response_body, truncate: _truncate } if *response_body => { request.context.extensions().with_lock(|ext| { ext.insert(DisplayRouterResponse); }); @@ -229,15 +239,32 @@ impl Selector for RouterSelector { fn on_response(&self, response: &router::Response) -> Option { match self { - RouterSelector::ResponseBody { response_body } if *response_body => { + RouterSelector::ResponseBody { response_body, truncate } if *response_body => { response .context .extensions() .with_lock(|ext| { - // Clone here in case anything else also needs access to the body ext.get::().cloned() }) - .map(|v| opentelemetry::Value::String(v.0.into())) + .and_then(|v| { + if let Some(truncate) = truncate { + from_str::(&v.0) + .ok() + .and_then(|body_json| { + let field_name = match truncate { + ResponseBodyField::Data => "data", + ResponseBodyField::Errors => "errors", + }; + body_json.get(field_name).cloned() + }) + .and_then(|truncated_body| { + to_string(&truncated_body).ok() + }) + .map(|s| opentelemetry::Value::String(s.into())) + } else { + Some(opentelemetry::Value::String(v.0.into())) + } + }) } RouterSelector::ResponseHeader { response_header, @@ -426,7 +453,7 @@ mod test { use crate::context::OPERATION_NAME; use crate::plugins::telemetry::TraceIdFormat; use crate::plugins::telemetry::config_new::Selector; - use crate::plugins::telemetry::config_new::router::selectors::RouterSelector; + use crate::plugins::telemetry::config_new::router::selectors::{ ResponseBodyField, RouterSelector }; use crate::plugins::telemetry::config_new::selectors::OperationName; use crate::plugins::telemetry::config_new::selectors::ResponseStatus; use crate::plugins::telemetry::otel; @@ -899,6 +926,7 @@ mod test { fn router_response_body() { let selector = RouterSelector::ResponseBody { response_body: true, + truncate: None, }; let res = &crate::services::RouterResponse::fake_builder() .status_code(StatusCode::OK) @@ -910,4 +938,28 @@ mod test { r#"{"data":"some data"}"# ); } + + #[test] + fn router_response_body_errors() { + let selector = RouterSelector::ResponseBody { + response_body: true, + truncate: Some(ResponseBodyField::Errors), + }; + 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"}}]"# + ); + } } From ce583451a118640cd42509739b7d9451b56fdfde Mon Sep 17 00:00:00 2001 From: Aguilarjaf Date: Fri, 11 Jul 2025 13:02:18 -0700 Subject: [PATCH 2/6] update schema gen snap --- ...ter__configuration__tests__schema_generation.snap | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 5b2a1e407d..1dbec01273 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 @@ -5374,6 +5374,13 @@ expression: "&schema" } ] }, + "ResponseBodyField": { + "enum": [ + "data", + "errors" + ], + "type": "string" + }, "ResponseStatus": { "oneOf": [ { @@ -5800,6 +5807,11 @@ expression: "&schema" "response_body": { "description": "The response body enabled or not", "type": "boolean" + }, + "truncate": { + "$ref": "#/definitions/ResponseBodyField", + "description": "#/definitions/ResponseBodyField", + "nullable": true } }, "required": [ From 7597805656b803b39ae5d598077a53267e2fb1df Mon Sep 17 00:00:00 2001 From: Jorge Aguilar <59518622+Aguilarjaf@users.noreply.github.com> Date: Mon, 14 Jul 2025 21:36:32 -0700 Subject: [PATCH 3/6] Update apollo-router/src/plugins/telemetry/config_new/router/selectors.rs Co-authored-by: Zelda Hessler --- .../src/plugins/telemetry/config_new/router/selectors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 275ffcb072..7885d51cf0 100644 --- a/apollo-router/src/plugins/telemetry/config_new/router/selectors.rs +++ b/apollo-router/src/plugins/telemetry/config_new/router/selectors.rs @@ -226,7 +226,7 @@ impl Selector for RouterSelector { } => get_baggage(baggage).or_else(|| default.maybe_to_otel_value()), RouterSelector::Static(val) => Some(val.clone().into()), RouterSelector::StaticField { r#static } => Some(r#static.clone().into()), - RouterSelector::ResponseBody { response_body, truncate: _truncate } if *response_body => { + RouterSelector::ResponseBody { response_body, .. } if *response_body => { request.context.extensions().with_lock(|ext| { ext.insert(DisplayRouterResponse); }); From 44f8779681a38292bfa61aace4e5df02789212b6 Mon Sep 17 00:00:00 2001 From: Aguilarjaf Date: Thu, 17 Jul 2025 23:42:17 -0700 Subject: [PATCH 4/6] feat: add ResponseErrors selector to extract router response errors --- ...nfiguration__tests__schema_generation.snap | 26 ++--- .../telemetry/config_new/router/selectors.rs | 94 +++++++++++-------- 2 files changed, 67 insertions(+), 53 deletions(-) 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 87392322a2..689d2ed87a 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 @@ -5429,13 +5429,6 @@ expression: "&schema" } ] }, - "ResponseBodyField": { - "enum": [ - "data", - "errors" - ], - "type": "string" - }, "ResponseStatus": { "oneOf": [ { @@ -5862,11 +5855,6 @@ expression: "&schema" "response_body": { "description": "The response body enabled or not", "type": "boolean" - }, - "truncate": { - "$ref": "#/definitions/ResponseBodyField", - "description": "#/definitions/ResponseBodyField", - "nullable": true } }, "required": [ @@ -5874,6 +5862,20 @@ expression: "&schema" ], "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 7885d51cf0..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,12 +1,15 @@ +use derivative::Derivative; use schemars::JsonSchema; use serde::Deserialize; -use serde_json::{from_str, to_string}; +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; @@ -23,13 +26,6 @@ use crate::plugins::telemetry::config_new::trace_id; use crate::query_planner::APOLLO_OPERATION_ID; use crate::services::router; -#[derive(Deserialize, JsonSchema, Clone, Debug, PartialEq)] -#[serde(rename_all = "snake_case")] -pub(crate) enum ResponseBodyField { - Data, - Errors, -} - #[derive(Deserialize, JsonSchema, Clone, Debug)] #[serde(deny_unknown_fields, rename_all = "snake_case", untagged)] pub(crate) enum RouterValue { @@ -46,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 { @@ -124,8 +121,14 @@ pub(crate) enum RouterSelector { ResponseBody { /// The response body enabled or not response_body: bool, - /// Option to truncate the response body - truncate: Option, + }, + /// 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 { @@ -178,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()) @@ -226,10 +236,12 @@ impl Selector for RouterSelector { } => get_baggage(baggage).or_else(|| default.maybe_to_otel_value()), 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); - }); + RouterSelector::ResponseBody { response_body } if *response_body => { + insert_display_router_response(request); + None + } + RouterSelector::ResponseErrors { .. } => { + insert_display_router_response(request); None } // Related to Response @@ -239,33 +251,34 @@ impl Selector for RouterSelector { fn on_response(&self, response: &router::Response) -> Option { match self { - RouterSelector::ResponseBody { response_body, truncate } if *response_body => { + RouterSelector::ResponseBody { response_body } if *response_body => { response .context .extensions() .with_lock(|ext| { + // Clone here in case anything else also needs access to the body ext.get::().cloned() }) - .and_then(|v| { - if let Some(truncate) = truncate { - from_str::(&v.0) - .ok() - .and_then(|body_json| { - let field_name = match truncate { - ResponseBodyField::Data => "data", - ResponseBodyField::Errors => "errors", - }; - body_json.get(field_name).cloned() - }) - .and_then(|truncated_body| { - to_string(&truncated_body).ok() - }) - .map(|s| opentelemetry::Value::String(s.into())) - } else { - Some(opentelemetry::Value::String(v.0.into())) - } - }) + .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, @@ -445,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; @@ -453,7 +467,7 @@ mod test { use crate::context::OPERATION_NAME; use crate::plugins::telemetry::TraceIdFormat; use crate::plugins::telemetry::config_new::Selector; - use crate::plugins::telemetry::config_new::router::selectors::{ ResponseBodyField, RouterSelector }; + use crate::plugins::telemetry::config_new::router::selectors::RouterSelector; use crate::plugins::telemetry::config_new::selectors::OperationName; use crate::plugins::telemetry::config_new::selectors::ResponseStatus; use crate::plugins::telemetry::otel; @@ -926,7 +940,6 @@ mod test { fn router_response_body() { let selector = RouterSelector::ResponseBody { response_body: true, - truncate: None, }; let res = &crate::services::RouterResponse::fake_builder() .status_code(StatusCode::OK) @@ -941,9 +954,8 @@ mod test { #[test] fn router_response_body_errors() { - let selector = RouterSelector::ResponseBody { - response_body: true, - truncate: Some(ResponseBodyField::Errors), + let selector = RouterSelector::ResponseErrors { + response_errors: JsonPathInst::new("$.[0]").unwrap(), }; let res = &crate::services::RouterResponse::fake_builder() .status_code(StatusCode::BAD_REQUEST) @@ -959,7 +971,7 @@ mod test { .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"}}]"# + r#"{"message":"Something went wrong","locations":[{"line":1,"column":1}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}"# ); } } From 51aac57172e47ca2868499ac3eb420f27b1b75b6 Mon Sep 17 00:00:00 2001 From: Aguilarjaf Date: Tue, 22 Jul 2025 15:48:59 -0700 Subject: [PATCH 5/6] add changeset entry for ResponseErrors --- ...t_aguilarjaf_add_response_errors_selector.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .changesets/feat_aguilarjaf_add_response_errors_selector.md 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 From 0b0968a37ba4187f520d1e3c642a38b2045517ff Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Mon, 25 Aug 2025 09:16:48 -0500 Subject: [PATCH 6/6] add docs --- .../telemetry/instrumentation/selectors.mdx | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/source/routing/observability/telemetry/instrumentation/selectors.mdx b/docs/source/routing/observability/telemetry/instrumentation/selectors.mdx index cfe50dc9a0..55f0ba10fd 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 | @@ -190,6 +192,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: