Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dfa3b2b
Adds request.extensions support for client name and version
calvincestari Oct 28, 2025
bfbb873
Adds changeset
calvincestari Oct 28, 2025
52afaa1
Adds library headers to telemetry collection
calvincestari Oct 31, 2025
e12601c
Update snapshot
calvincestari Oct 31, 2025
8ea50b8
Merge branch 'dev' into feature/client-awareness-common-transport
calvincestari Nov 3, 2025
7ade61b
Merge branch 'dev' into feature/client-awareness-common-transport
calvincestari Nov 4, 2025
0476fa5
Merge branch 'dev' into feature/client-awareness-common-transport
calvincestari Nov 4, 2025
c2e2659
Adds HTTP header metrics tests
calvincestari Nov 6, 2025
51b047f
Adds new test snapshots
calvincestari Nov 6, 2025
e14a07d
Cleanup old snapshots
calvincestari Nov 6, 2025
8211e3d
Merge branch 'dev' into feature/client-awareness-common-transport
calvincestari Nov 6, 2025
00496da
Merge branch 'dev' into feature/client-awareness-common-transport
calvincestari Nov 6, 2025
4b8daa2
Fix linting error
calvincestari Nov 6, 2025
36ba83a
Merge branch 'dev' into feature/client-awareness-common-transport
calvincestari Nov 6, 2025
0c8339e
Merge branch 'dev' into feature/client-awareness-common-transport
calvincestari Nov 6, 2025
b8574f3
Merge branch 'dev' into feature/client-awareness-common-transport
calvincestari Nov 7, 2025
2f5bbbc
Apply suggestion from @calvincestari
calvincestari Nov 7, 2025
7b6e454
Apply suggestion from @calvincestari
calvincestari Nov 7, 2025
dd23372
Merge branch 'dev' into feature/client-awareness-common-transport
calvincestari Nov 7, 2025
8a92fe3
Merge branch 'dev' into feature/client-awareness-common-transport
calvincestari Nov 10, 2025
994fbc4
Update configuration snapshot
calvincestari Nov 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### ([PR #8503](https://github.com/apollographql/router/pull/8503))

Supporting a common transport mechanism for the Client Awareness and Enhanced Client Awareness values is more efficient than the current split between HTTP header (for Client Awareness) and `request.extensions` for Enhanced Client Awareness. This changeset allows clients to send both sets of values using the same method.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: which one 'wins' if for instance clientLibrary is provided both in headers and in extensions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My assumption is the extensions plugin would overwrite any header values but I'm not certain; I'd need to check the order of request processing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circling back to this - I believe my assumption is correct.

  • Looking at the request lifecycle detailed in the Router docs the router service is before the supergraph service.
  • The telemetry plugin is registered for interaction with many services but the one that handles the client name/version extraction is in the router service logic - here
  • The enhanced client awareness plugin in registered only for interaction with the supergraph service - here

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for confirming. I guess it should be documented somewhere (not sure where!) to avoid any mixup.


By [@calvincestari](https://github.com/calvincestari).
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,22 @@ expression: "&schema"
],
"description": "Field level instrumentation for subgraphs via ftv1. ftv1 tracing can cause performance issues as it is transmitted in band with subgraph responses."
},
"library_name_header": {
"default": "apollographql-library-name",
"description": "The name of the header to extract from requests when populating 'library name' for traces and metrics in Apollo Studio.",
"type": [
"string",
"null"
]
},
"library_version_header": {
"default": "apollographql-library-version",
"description": "The name of the header to extract from requests when populating 'library version' for traces and metrics in Apollo Studio.",
"type": [
"string",
"null"
]
},
"metrics": {
"allOf": [
{
Expand Down
34 changes: 30 additions & 4 deletions apollo-router/src/plugins/enhanced_client_awareness/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ use crate::plugin::Plugin;
use crate::plugin::PluginInit;
use crate::plugins::telemetry::CLIENT_LIBRARY_NAME;
use crate::plugins::telemetry::CLIENT_LIBRARY_VERSION;
use crate::plugins::telemetry::CLIENT_NAME;
use crate::plugins::telemetry::CLIENT_VERSION;
use crate::services::supergraph;

const CLIENT_LIBRARY_KEY: &str = "clientLibrary";
const CLIENT_LIBRARY_NAME_KEY: &str = "name";
const CLIENT_LIBRARY_VERSION_KEY: &str = "version";
const CLIENT_APP_KEY: &str = "clientApp";
const CLIENT_NAME_KEY: &str = "name";
const CLIENT_VERSION_KEY: &str = "version";

/// The enhanced client-awareness plugin has no configuration.
#[derive(Debug, Deserialize, JsonSchema)]
Expand Down Expand Up @@ -40,7 +43,7 @@ impl Plugin for EnhancedClientAwareness {
.get(CLIENT_LIBRARY_KEY)
{
if let Some(client_library_name) = client_library_metadata
.get(CLIENT_LIBRARY_NAME_KEY)
.get(CLIENT_NAME_KEY)
.and_then(|value| value.as_str())
{
let _ = request
Expand All @@ -49,7 +52,7 @@ impl Plugin for EnhancedClientAwareness {
};

if let Some(client_library_version) = client_library_metadata
.get(CLIENT_LIBRARY_VERSION_KEY)
.get(CLIENT_VERSION_KEY)
.and_then(|value| value.as_str())
{
let _ = request
Expand All @@ -58,6 +61,29 @@ impl Plugin for EnhancedClientAwareness {
};
};

if let Some(client_app_metadata) = request
.supergraph_request
.body()
.extensions
.get(CLIENT_APP_KEY)
{
if let Some(client_name) = client_app_metadata
.get(CLIENT_NAME_KEY)
.and_then(|value| value.as_str())
{
let _ = request.context.insert(CLIENT_NAME, client_name.to_string());
};

if let Some(client_version) = client_app_metadata
.get(CLIENT_VERSION_KEY)
.and_then(|value| value.as_str())
{
let _ = request
.context
.insert(CLIENT_VERSION, client_version.to_string());
};
};

request
})
.service(service)
Expand Down
97 changes: 90 additions & 7 deletions apollo-router/src/plugins/enhanced_client_awareness/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ use crate::Context;
use crate::plugin::Plugin;
use crate::plugin::PluginInit;
use crate::plugin::test::MockSupergraphService;
use crate::plugins::enhanced_client_awareness::CLIENT_APP_KEY;
use crate::plugins::enhanced_client_awareness::CLIENT_LIBRARY_KEY;
use crate::plugins::enhanced_client_awareness::CLIENT_LIBRARY_NAME_KEY;
use crate::plugins::enhanced_client_awareness::CLIENT_LIBRARY_VERSION_KEY;
use crate::plugins::enhanced_client_awareness::CLIENT_NAME_KEY;
use crate::plugins::enhanced_client_awareness::CLIENT_VERSION_KEY;
use crate::plugins::enhanced_client_awareness::Config;
use crate::plugins::telemetry::CLIENT_LIBRARY_NAME;
use crate::plugins::telemetry::CLIENT_LIBRARY_VERSION;
use crate::plugins::telemetry::CLIENT_NAME;
use crate::plugins::telemetry::CLIENT_VERSION;
use crate::services::SupergraphResponse;
use crate::services::supergraph;

Expand Down Expand Up @@ -53,11 +56,8 @@ async fn given_client_library_metadata_adds_values_to_context() {

// given
let mut clients_map = serde_json_bytes::map::Map::new();
clients_map.insert(
CLIENT_LIBRARY_NAME_KEY,
"apollo-general-client-library".into(),
);
clients_map.insert(CLIENT_LIBRARY_VERSION_KEY, "0.1.0".into());
clients_map.insert(CLIENT_NAME_KEY, "apollo-general-client-library".into());
clients_map.insert(CLIENT_VERSION_KEY, "0.1.0".into());
let mut extensions_map = serde_json_bytes::map::Map::new();
extensions_map.insert(CLIENT_LIBRARY_KEY, clients_map.into());

Expand Down Expand Up @@ -99,3 +99,86 @@ async fn without_client_library_metadata_does_not_add_values_to_context() {

let _ = service_stack.oneshot(request).await;
}

#[tokio::test]
async fn given_client_app_metadata_adds_values_to_context() {
let mut mock_service = MockSupergraphService::new();

mock_service.expect_call().returning(move |request| {
// then
assert!(
request.context.contains_key(CLIENT_NAME),
"Missing CLIENT_NAME key/value"
);
let client_name: String = request
.context
.get(CLIENT_NAME)
.unwrap_or_default()
.unwrap_or_default();
assert_eq!(client_name, "apollo-general-client");

assert!(
request.context.contains_key(CLIENT_VERSION),
"Missing CLIENT_VERSION key/value"
);
let client_version: String = request
.context
.get(CLIENT_VERSION)
.unwrap_or_default()
.unwrap_or_default();
assert_eq!(client_version, "0.1.0");

SupergraphResponse::fake_builder().build()
});

let service_stack =
EnhancedClientAwareness::new(PluginInit::fake_new(Config {}, Default::default()))
.await
.unwrap()
.supergraph_service(mock_service.boxed());

// given
let mut clients_map = serde_json_bytes::map::Map::new();
clients_map.insert(CLIENT_NAME_KEY, "apollo-general-client".into());
clients_map.insert(CLIENT_VERSION_KEY, "0.1.0".into());
let mut extensions_map = serde_json_bytes::map::Map::new();
extensions_map.insert(CLIENT_APP_KEY, clients_map.into());

// when
let request = supergraph::Request::fake_builder()
.context(Context::default())
.query("{query:{ foo { bar } }}")
.extensions(extensions_map)
.build()
.unwrap();

let _ = service_stack.oneshot(request).await;
}

#[tokio::test]
async fn without_client_app_metadata_does_not_add_values_to_context() {
let mut mock_service = MockSupergraphService::new();

mock_service.expect_call().returning(move |request| {
// then
assert!(!request.context.contains_key(CLIENT_NAME));
assert!(!request.context.contains_key(CLIENT_VERSION));

SupergraphResponse::fake_builder().build()
});

let service_stack =
EnhancedClientAwareness::new(PluginInit::fake_new(Config {}, Default::default()))
.await
.unwrap()
.supergraph_service(mock_service.boxed());

// when
let request = supergraph::Request::fake_builder()
.context(Context::default())
.query("{query:{ foo { bar } }}")
.build()
.unwrap();

let _ = service_stack.oneshot(request).await;
}
31 changes: 31 additions & 0 deletions apollo-router/src/plugins/telemetry/apollo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ pub(crate) struct Config {
#[serde(deserialize_with = "deserialize_header_name")]
pub(crate) client_version_header: HeaderName,

/// The name of the header to extract from requests when populating 'library name' for traces and metrics in Apollo Studio.
#[schemars(with = "Option<String>", default = "library_name_header_default_str")]
#[serde(deserialize_with = "deserialize_header_name")]
pub(crate) library_name_header: HeaderName,

/// The name of the header to extract from requests when populating 'library version' for traces and metrics in Apollo Studio.
#[schemars(
with = "Option<String>",
default = "library_version_header_default_str"
)]
#[serde(deserialize_with = "deserialize_header_name")]
pub(crate) library_version_header: HeaderName,

/// The buffer size for sending traces to Apollo. Increase this if you are experiencing lost traces.
pub(crate) buffer_size: NonZeroUsize,

Expand Down Expand Up @@ -355,6 +368,22 @@ const fn client_version_header_default() -> HeaderName {
HeaderName::from_static(client_version_header_default_str())
}

const fn library_name_header_default_str() -> &'static str {
"apollographql-library-name"
}

const fn library_name_header_default() -> HeaderName {
HeaderName::from_static(library_name_header_default_str())
}

const fn library_version_header_default_str() -> &'static str {
"apollographql-library-version"
}

const fn library_version_header_default() -> HeaderName {
HeaderName::from_static(library_version_header_default_str())
}

pub(crate) const fn default_buffer_size() -> NonZeroUsize {
unsafe { NonZeroUsize::new_unchecked(10000) }
}
Expand All @@ -370,6 +399,8 @@ impl Default for Config {
apollo_graph_ref: apollo_graph_reference(),
client_name_header: client_name_header_default(),
client_version_header: client_version_header_default(),
library_name_header: library_name_header_default(),
library_version_header: library_version_header_default(),
schema_id: "<no_schema_id>".to_string(),
buffer_size: default_buffer_size(),
field_level_instrumentation_sampler: default_field_level_instrumentation_sampler(),
Expand Down
21 changes: 21 additions & 0 deletions apollo-router/src/plugins/telemetry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,27 @@ impl PluginPrivate for Telemetry {
let _ = request.context.insert(CLIENT_VERSION, version.to_owned());
}

let library_name = request
.router_request
.headers()
.get(&config_request.apollo.library_name_header)
.and_then(|h| h.to_str().ok());
let library_version = request
.router_request
.headers()
.get(&config_request.apollo.library_version_header)
.and_then(|h| h.to_str().ok());

if let Some(name) = library_name {
let _ = request.context.insert(CLIENT_LIBRARY_NAME, name.to_owned());
}

if let Some(version) = library_version {
let _ = request
.context
.insert(CLIENT_LIBRARY_VERSION, version.to_owned());
}

let mut custom_attributes = config_request
.instrumentation
.spans
Expand Down
56 changes: 54 additions & 2 deletions apollo-router/tests/apollo_reports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ async fn test_batch_trace_id() {
}

#[tokio::test(flavor = "multi_thread")]
async fn test_client_name() {
async fn test_trace_with_client_name() {
for use_legacy_request_span in [true, false] {
let request = supergraph::Request::fake_builder()
.query("query{topProducts{name reviews {author{name}} reviews{author{name}}}}")
Expand All @@ -579,7 +579,7 @@ async fn test_client_name() {
}

#[tokio::test(flavor = "multi_thread")]
async fn test_client_version() {
async fn test_trace_with_client_version() {
for use_legacy_request_span in [true, false] {
let request = supergraph::Request::fake_builder()
.query("query{topProducts{name reviews {author{name}} reviews{author{name}}}}")
Expand All @@ -594,6 +594,58 @@ async fn test_client_version() {
}
}

#[tokio::test(flavor = "multi_thread")]
async fn test_metrics_with_client_name() {
let request = supergraph::Request::fake_builder()
.query("query{topProducts{name reviews {author{name}} reviews{author{name}}}}")
.header("apollographql-client-name", "my client")
.build()
.unwrap();
let req: router::Request = request.try_into().expect("could not convert request");
let reports = Arc::new(Mutex::new(vec![]));
let report = get_metrics_report(reports, req, false, false, None).await;
assert_report!(report);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_metrics_with_client_version() {
let request = supergraph::Request::fake_builder()
.query("query{topProducts{name reviews {author{name}} reviews{author{name}}}}")
.header("apollographql-client-version", "my client version")
.build()
.unwrap();
let req: router::Request = request.try_into().expect("could not convert request");
let reports = Arc::new(Mutex::new(vec![]));
let report = get_metrics_report(reports, req, false, false, None).await;
assert_report!(report);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_metrics_with_library_name() {
let request = supergraph::Request::fake_builder()
.query("query{topProducts{name reviews {author{name}} reviews{author{name}}}}")
.header("apollographql-library-name", "apollo library")
.build()
.unwrap();
let req: router::Request = request.try_into().expect("could not convert request");
let reports = Arc::new(Mutex::new(vec![]));
let report = get_metrics_report(reports, req, false, false, None).await;
assert_report!(report);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_metrics_with_library_version() {
let request = supergraph::Request::fake_builder()
.query("query{topProducts{name reviews {author{name}} reviews{author{name}}}}")
.header("apollographql-library-version", "0.1.0")
.build()
.unwrap();
let req: router::Request = request.try_into().expect("could not convert request");
let reports = Arc::new(Mutex::new(vec![]));
let report = get_metrics_report(reports, req, false, false, None).await;
assert_report!(report);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_send_header() {
for use_legacy_request_span in [true, false] {
Expand Down
Loading