From a7fcdd4778730563e4657e90262dadddebefeb60 Mon Sep 17 00:00:00 2001 From: Sze Ching Date: Wed, 28 Jan 2026 17:50:22 +0000 Subject: [PATCH] feat(graph-proxy): add request duration as observable metrics --- backend/graph-proxy/src/graphql/mod.rs | 26 ++++++++++++++++++++++++-- backend/graph-proxy/src/metrics.rs | 15 +++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/backend/graph-proxy/src/graphql/mod.rs b/backend/graph-proxy/src/graphql/mod.rs index 76f97543a..1806c5042 100644 --- a/backend/graph-proxy/src/graphql/mod.rs +++ b/backend/graph-proxy/src/graphql/mod.rs @@ -116,7 +116,9 @@ pub async fn graphql_handler( auth_token_header: Option>>, request: GraphQLRequest, ) -> GraphQLResponse { + let start = std::time::Instant::now(); let query = request.into_inner(); + let mut request_type = "unparseable"; if let Ok(query) = parse_query(&query.query) { let operation = query.operations; @@ -130,19 +132,24 @@ pub async fn graphql_handler( .map(|operation| operation.1.node.ty) .collect(), }; + let mut has_mutation = false; for operation in operations { match operation { async_graphql::parser::types::OperationType::Query => state .metrics_state .total_requests .add(1, &[KeyValue::new("request_type", "query")]), - async_graphql::parser::types::OperationType::Mutation => state + async_graphql::parser::types::OperationType::Mutation => { + has_mutation = true; + state .metrics_state .total_requests - .add(1, &[KeyValue::new("request_type", "mutation")]), + .add(1, &[KeyValue::new("request_type", "mutation")]) + }, async_graphql::parser::types::OperationType::Subscription => {} }; } + request_type = if has_mutation { "mutation" } else { "query" }; } else { state .metrics_state @@ -152,6 +159,21 @@ pub async fn graphql_handler( let auth_token = auth_token_header.map(|header| header.0); state.schema.execute(query.data(auth_token)).await.into() + let response = state.schema.execute(query.data(auth_token)).await; + let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0; + let status = if response.errors.is_empty() { + "ok" + } else { + "error" + }; + state.metrics_state.request_duration_ms.record( + elapsed_ms, + &[ + KeyValue::new("request_type", request_type), + KeyValue::new("status", status), + ], + ); + response.into() } lazy_static! { diff --git a/backend/graph-proxy/src/metrics.rs b/backend/graph-proxy/src/metrics.rs index e61ed555f..458fc38d7 100644 --- a/backend/graph-proxy/src/metrics.rs +++ b/backend/graph-proxy/src/metrics.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use opentelemetry::metrics::{Counter, MeterProvider}; +use opentelemetry::metrics::{Counter, Histogram, MeterProvider}; use opentelemetry_sdk::metrics::SdkMeterProvider; /// Thread-safe wrapper for OTEL metrics @@ -11,6 +11,8 @@ pub type MetricsState = Arc; pub struct Metrics { /// Total requests on all routes pub total_requests: Counter, + /// Request duration in miliseconds on every request + pub request_duration_ms: Histogram, } impl Metrics { @@ -23,6 +25,15 @@ impl Metrics { .with_description("The total requests on all routes made since the last restart.") .build(); - Metrics { total_requests } + let request_duration_ms = meter + .f64_histogram("graph_proxy_request_duration_ms") + .with_description("GraphQL request duration") + .with_unit("ms") + .build(); + + Metrics { + total_requests, + request_duration_ms, + } } }