From 1d20e72caf54bac4f6d67364369e4edcf520af52 Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Tue, 28 May 2024 14:17:28 +0100 Subject: [PATCH] Add support for operation name on graphql metrics. (#5257) Co-authored-by: bryn --- .../exp_experimental_graphql_instruments.md | 12 ++-- ...nfiguration__tests__schema_generation.snap | 30 ++++++++ .../config_new/graphql/attributes.rs | 33 +++++++-- .../telemetry/config_new/graphql/selectors.rs | 70 ++++++++++++++++++- 4 files changed, 134 insertions(+), 11 deletions(-) diff --git a/.changesets/exp_experimental_graphql_instruments.md b/.changesets/exp_experimental_graphql_instruments.md index a44ec7e229..a8714083a3 100644 --- a/.changesets/exp_experimental_graphql_instruments.md +++ b/.changesets/exp_experimental_graphql_instruments.md @@ -1,6 +1,6 @@ -### Add graphql instruments ([PR #5215](https://github.com/apollographql/router/pull/5215)) +### Add graphql instruments ([PR #5215](https://github.com/apollographql/router/pull/5215), [PR #5257](https://github.com/apollographql/router/pull/5257)) -This PR adds experimental GraphQL instruments to telemetry as a commercial feature. +This PR adds experimental GraphQL instruments to telemetry. It makes the following possible: ``` @@ -47,7 +47,9 @@ telemetry: - "topProducts" ``` -Note that users should not use this feature yet as it will have performance issues, may cause excessive metrics to be generated and we may change or remove this feature. -It is for experimental purposes only and is not supported in production environments. +Note that this will have a significant performance impact which will be addressed in a following release. +Users should also be aware that large numbers of excessive metrics may be generated, and they should take care not to run up a large APM bill. -By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5215 +For now, do not use these metrics in production. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5215 and https://github.com/apollographql/router/pull/5257 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 b820b8e2a0..cce464f63e 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 @@ -2801,6 +2801,12 @@ expression: "&schema" "nullable": true, "type": "boolean" }, + "graphql.operation.name": { + "default": null, + "description": "The GraphQL operation name", + "nullable": true, + "type": "boolean" + }, "graphql.type.name": { "default": null, "description": "The GraphQL type name", @@ -2882,6 +2888,24 @@ expression: "&schema" ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "default": { + "description": "Optional default value.", + "nullable": true, + "type": "string" + }, + "operation_name": { + "$ref": "#/definitions/OperationName", + "description": "#/definitions/OperationName" + } + }, + "required": [ + "operation_name" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { @@ -7638,6 +7662,12 @@ expression: "&schema" "nullable": true, "type": "boolean" }, + "graphql.operation.name": { + "default": null, + "description": "The GraphQL operation name", + "nullable": true, + "type": "boolean" + }, "graphql.type.name": { "default": null, "description": "The GraphQL type name", diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs index d440ad9360..36f1b568a3 100644 --- a/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs @@ -9,6 +9,7 @@ use crate::plugins::telemetry::config_new::graphql::selectors::FieldType; use crate::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector; use crate::plugins::telemetry::config_new::graphql::selectors::ListLength; use crate::plugins::telemetry::config_new::graphql::selectors::TypeName; +use crate::plugins::telemetry::config_new::selectors::OperationName; use crate::plugins::telemetry::config_new::DefaultAttributeRequirementLevel; use crate::plugins::telemetry::config_new::DefaultForLevel; use crate::plugins::telemetry::config_new::Selector; @@ -29,6 +30,9 @@ pub(crate) struct GraphQLAttributes { /// If the field is a list, the length of the list #[serde(rename = "graphql.list.length")] pub(crate) list_length: Option, + /// The GraphQL operation name + #[serde(rename = "graphql.operation.name")] + pub(crate) operation_name: Option, /// The GraphQL type name #[serde(rename = "graphql.type.name")] pub(crate) type_name: Option, @@ -105,17 +109,29 @@ impl Selectors for GraphQLAttributes { attrs.push(KeyValue::new("graphql.list.length", length)); } } + if let Some(true) = self.operation_name { + if let Some(length) = (GraphQLSelector::OperationName { + operation_name: OperationName::String, + default: None, + }) + .on_response_field(typed_value, ctx) + { + attrs.push(KeyValue::new("graphql.operation.name", length)); + } + } attrs } } #[cfg(test)] mod test { + use crate::context::OPERATION_NAME; use crate::plugins::demand_control::cost_calculator::schema_aware_response::TypedValue; use crate::plugins::telemetry::config_new::test::field; use crate::plugins::telemetry::config_new::test::ty; use crate::plugins::telemetry::config_new::DefaultForLevel; use crate::plugins::telemetry::config_new::Selectors; + use crate::Context; #[test] fn test_default_for_level() { @@ -128,6 +144,7 @@ mod test { assert_eq!(attributes.field_type, Some(true)); assert_eq!(attributes.type_name, Some(true)); assert_eq!(attributes.list_length, None); + assert_eq!(attributes.operation_name, None); } #[test] @@ -136,18 +153,22 @@ mod test { field_name: Some(true), field_type: Some(true), list_length: Some(true), + operation_name: Some(true), type_name: Some(true), }; let typed_value = TypedValue::Bool(ty(), field(), &true); - let ctx = Default::default(); + let ctx = Context::default(); + let _ = ctx.insert(OPERATION_NAME, "operation_name".to_string()); let result = attributes.on_response_field(&typed_value, &ctx); - assert_eq!(result.len(), 3); + assert_eq!(result.len(), 4); assert_eq!(result[0].key.as_str(), "graphql.field.name"); assert_eq!(result[0].value.as_str(), "field_name"); assert_eq!(result[1].key.as_str(), "graphql.field.type"); assert_eq!(result[1].value.as_str(), "field_type"); assert_eq!(result[2].key.as_str(), "graphql.type.name"); assert_eq!(result[2].value.as_str(), "type_name"); + assert_eq!(result[3].key.as_str(), "graphql.operation.name"); + assert_eq!(result[3].value.as_str(), "operation_name"); } #[test] @@ -156,6 +177,7 @@ mod test { field_name: Some(true), field_type: Some(true), list_length: Some(true), + operation_name: Some(true), type_name: Some(true), }; let typed_value = TypedValue::List( @@ -167,9 +189,10 @@ mod test { TypedValue::Bool(ty(), field(), &true), ], ); - let ctx = Default::default(); + let ctx = Context::default(); + let _ = ctx.insert(OPERATION_NAME, "operation_name".to_string()); let result = attributes.on_response_field(&typed_value, &ctx); - assert_eq!(result.len(), 4); + assert_eq!(result.len(), 5); assert_eq!(result[0].key.as_str(), "graphql.field.name"); assert_eq!(result[0].value.as_str(), "field_name"); assert_eq!(result[1].key.as_str(), "graphql.field.type"); @@ -178,5 +201,7 @@ mod test { assert_eq!(result[2].value.as_str(), "type_name"); assert_eq!(result[3].key.as_str(), "graphql.list.length"); assert_eq!(result[3].value.as_str(), "3"); + assert_eq!(result[4].key.as_str(), "graphql.operation.name"); + assert_eq!(result[4].value.as_str(), "operation_name"); } } diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/selectors.rs b/apollo-router/src/plugins/telemetry/config_new/graphql/selectors.rs index f0323759d4..46cf6f5c44 100644 --- a/apollo-router/src/plugins/telemetry/config_new/graphql/selectors.rs +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/selectors.rs @@ -1,9 +1,12 @@ use schemars::JsonSchema; use serde::Deserialize; +use sha2::Digest; use tower::BoxError; +use crate::context::OPERATION_NAME; use crate::plugins::demand_control::cost_calculator::schema_aware_response::TypedValue; use crate::plugins::telemetry::config::AttributeValue; +use crate::plugins::telemetry::config_new::selectors::OperationName; use crate::plugins::telemetry::config_new::Selector; use crate::Context; @@ -65,7 +68,12 @@ pub(crate) enum GraphQLSelector { #[allow(dead_code)] type_name: TypeName, }, - + OperationName { + /// The operation name from the query. + operation_name: OperationName, + /// Optional default value. + default: Option, + }, StaticField { /// A static value r#static: AttributeValue, @@ -92,7 +100,7 @@ impl Selector for GraphQLSelector { fn on_response_field( &self, typed_value: &TypedValue, - _ctx: &Context, + ctx: &Context, ) -> Option { match self { GraphQLSelector::ListLength { .. } => match typed_value { @@ -142,6 +150,22 @@ impl Selector for GraphQLSelector { TypedValue::Root(_) => None, }, GraphQLSelector::StaticField { r#static } => Some(r#static.clone().into()), + GraphQLSelector::OperationName { + operation_name, + default, + } => { + let op_name = ctx.get(OPERATION_NAME).ok().flatten(); + match operation_name { + OperationName::String => op_name.or_else(|| default.clone()), + OperationName::Hash => op_name.or_else(|| default.clone()).map(|op_name| { + let mut hasher = sha2::Sha256::new(); + hasher.update(op_name.as_bytes()); + let result = hasher.finalize(); + hex::encode(result) + }), + } + .map(opentelemetry::Value::from) + } } } } @@ -260,4 +284,46 @@ mod tests { let result = selector.on_response_field(&typed_value, &Context::default()); assert_eq!(result, Some(Value::String("static_value".into()))); } + + #[test] + fn operation_name() { + let selector = GraphQLSelector::OperationName { + operation_name: OperationName::String, + default: None, + }; + let typed_value = TypedValue::Bool(ty(), field(), &true); + let ctx = Context::default(); + let _ = ctx.insert(OPERATION_NAME, "some-operation".to_string()); + let result = selector.on_response_field(&typed_value, &ctx); + assert_eq!(result, Some(Value::String("some-operation".into()))); + } + + #[test] + fn operation_name_hash() { + let selector = GraphQLSelector::OperationName { + operation_name: OperationName::Hash, + default: None, + }; + let typed_value = TypedValue::Bool(ty(), field(), &true); + let ctx = Context::default(); + let _ = ctx.insert(OPERATION_NAME, "some-operation".to_string()); + let result = selector.on_response_field(&typed_value, &ctx); + assert_eq!( + result, + Some(Value::String( + "1d507f770a74cffd6cb014b190ea31160d442ff41d9bde088b634847aeafaafd".into() + )) + ); + } + + #[test] + fn operation_name_defaulted() { + let selector = GraphQLSelector::OperationName { + operation_name: OperationName::String, + default: Some("no-operation".to_string()), + }; + let typed_value = TypedValue::Bool(ty(), field(), &true); + let result = selector.on_response_field(&typed_value, &Context::default()); + assert_eq!(result, Some(Value::String("no-operation".into()))); + } }