Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
46fe85e
feat: adding ability to omit attributes for traces and metrics
alocay Sep 12, 2025
9a197bf
chore: adding changeset
alocay Sep 12, 2025
6ed890a
chore: updating changeset text
alocay Sep 12, 2025
d968c44
chore: removing unused file
alocay Sep 12, 2025
4bf5779
chore: renaming exporter file
alocay Sep 12, 2025
29e6ed9
chore: renaming module
alocay Sep 12, 2025
d1322b9
re-running checks
alocay Sep 12, 2025
0245966
chore: auto-gen the as_str function using enums instead of constants
alocay Sep 15, 2025
99349e3
chore: updating changeset entry and fixing fields in instruemnt attri…
alocay Sep 15, 2025
9866f71
chore: adding description as a doc string
alocay Sep 15, 2025
0986bba
chore: updating changeset entry
alocay Sep 15, 2025
1a97f1b
chore: updating operation attribute descriptions
alocay Sep 15, 2025
85e85cb
chore: updating operation_type attribute to operation_source
alocay Sep 15, 2025
67e78ba
chore: skipping request from instrumentation
alocay Sep 15, 2025
274799c
chore: fixing clippy issues
alocay Sep 15, 2025
d57f96f
re-running nix build
alocay Sep 16, 2025
55b640c
updating unit test snapshot
alocay Sep 16, 2025
7e2c0b2
chore: fixing toml formatting issues
alocay Sep 16, 2025
1be317f
test: removing asserts on constants
alocay Sep 16, 2025
db29544
chore: format issues
alocay Sep 16, 2025
fe924cb
test: adjusting unit test to not use local envar
alocay Sep 16, 2025
e9afe18
revert: undoing accidental commit
alocay Sep 16, 2025
a2b4f44
test: removing unnecessary assert
alocay Sep 16, 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
50 changes: 50 additions & 0 deletions .changesets/feat_allow_attribute_configuration_alocay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
### feat: adding ability to omit attributes for traces and metrics - @alocay PR #358

Adding ability to configure which attributes are omitted from telemetry traces and metrics.

1. Using a Rust build script (`build.rs`) to auto-generate telemetry attribute code based on the data found in `telemetry.toml`.
2. Utilizing an enum for attributes so typos in the config file raise an error.
3. Omitting trace attributes by filtering it out in a custom exporter.
4. Omitting metric attributes by indicating which attributes are allowed via a view.
5. Created `telemetry_attributes.rs` to map `TelemetryAttribute` enum to a OTEL `Key`.

The `telemetry.toml` file includes attributes (both for metrics and traces) as well as list of metrics gathered. An example would look like the following:
```
[apollo.mcp.attribute]
my_attribute = "Some attribute info"

[apollo.mcp.metric]
some.count = "Some metric count info"
```
This would generate a file that looks like the following:
```
use schemars::JsonSchema;
use serde::Deserialize;
pub const ALL_ATTRS: &[TelemetryAttribute; 1usize] = &[
TelemetryAttribute::MyAttribute
];
#[derive(Debug, Deserialize, JsonSchema, Clone, Eq, PartialEq, Hash, Copy)]
pub enum TelemetryAttribute {
#[serde(alias = "my_attribute")]
MyAttribute,
}
pub const APOLLO_MCP_ATTRIBUTE_MY_ATTRIBUTE: &str = "apollo.mcp.attribute.my_attribute";
pub const APOLLO_MCP_METRIC_SOME_COUNT: &str = "apollo.mcp.metric.some.count";
```
The configuration for this would look like the following:
```
telemetry:
exporters:
metrics:
otlp:
endpoint: "http://localhost:4317"
protocol: "grpc"
omitted_attributes:
- tool_name
tracing:
otlp:
endpoint: "http://localhost:4317"
protocol: "grpc"
omitted_attributes:
- request_id
```
55 changes: 54 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion crates/apollo-mcp-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ license-file.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
build = "build.rs"

default-run = "apollo-mcp-server"

Expand Down Expand Up @@ -41,7 +42,7 @@ opentelemetry-otlp = { version = "0.30.0", features = [
opentelemetry-resource-detectors = "0.9.0"
opentelemetry-semantic-conventions = "0.30.0"
opentelemetry-stdout = "0.30.0"
opentelemetry_sdk = "0.30.0"
opentelemetry_sdk = { version = "0.30.0", features = ["spec_unstable_metrics_views"] }
regex = "1.11.1"
reqwest-middleware = "0.4.2"
reqwest-tracing = { version = "0.5.8", features = ["opentelemetry_0_30"] }
Expand All @@ -65,6 +66,7 @@ tracing-opentelemetry = "0.31.0"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing.workspace = true
url.workspace = true
async-trait = "0.1.89"

[dev-dependencies]
chrono = { version = "0.4.41", default-features = false, features = ["now"] }
Expand All @@ -76,6 +78,13 @@ rstest.workspace = true
tokio.workspace = true
tracing-test = "0.2.5"

[build-dependencies]
serde.workspace = true
toml = "0.9.5"
quote = "1.0.40"
syn = "2.0.106"
prettyplease = "0.2.37"

[lints]
workspace = true

Expand Down
148 changes: 148 additions & 0 deletions crates/apollo-mcp-server/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
#![allow(clippy::panic)]

//! Build Script for the Apollo MCP Server
//!
//! This mostly compiles all the available telemetry attributes
use quote::__private::TokenStream;
use quote::quote;
use std::collections::hash_map::Keys;
use std::io::Write;
use std::iter::Map;
use std::{
collections::{HashMap, VecDeque},
io::Read as _,
};
use syn::parse2;

fn snake_to_pascal(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut up = true;
for ch in s.chars() {
if ch == '_' || ch == '-' || ch == ' ' {
up = true;
} else {
if up {
out.extend(ch.to_uppercase());
} else {
out.push(ch);
}
up = false;
}
}
out
}

type TokenStreamBuilder = fn(&Vec<String>) -> TokenStream;

fn generate_const_values_from_keys(
keys: Keys<Vec<String>, String>,
) -> Map<Keys<Vec<String>, String>, TokenStreamBuilder> {
keys.map(|key| {
let ident = key
.iter()
.map(|k| k.to_uppercase())
.collect::<Vec<_>>()
.join("_");
let ident = quote::format_ident!("{}", ident);
let value = key.join(".");

quote! {
pub const #ident: &str = #value;
}
})
}

fn main() {
// Parse the telemetry file
let telemetry: toml::Table = {
let mut raw = String::new();
std::fs::File::open("telemetry.toml")
.expect("could not open telemetry file")
.read_to_string(&mut raw)
.expect("could not read telemetry file");

toml::from_str(&raw).expect("could not parse telemetry file")
};

// Generate the keys
let mut attribute_keys = HashMap::new();
let mut attribute_enum_values = HashMap::new();
let mut metric_keys = HashMap::new();
let mut to_visit =
VecDeque::from_iter(telemetry.into_iter().map(|(key, val)| (vec![key], val)));
while let Some((key, value)) = to_visit.pop_front() {
match value {
toml::Value::String(val) => {
if key.contains(&"attribute".to_string()) {
let last_key = key.last().unwrap().clone();
attribute_enum_values.insert(snake_to_pascal(last_key.as_str()), last_key);
attribute_keys.insert(key, val);
} else {
metric_keys.insert(key, val);
}
}
toml::Value::Table(map) => to_visit.extend(
map.into_iter()
.map(|(nested_key, value)| ([key.clone(), vec![nested_key]].concat(), value)),
),

_ => panic!("telemetry values should be string descriptions"),
};
}

println!(
"{:?} | {:?} | {:?}",
metric_keys, attribute_keys, attribute_enum_values
);

// Write out the generated keys
let out_dir = std::env::var_os("OUT_DIR").expect("could not retrieve output directory");
let dest_path = std::path::Path::new(&out_dir).join("telemetry_attributes.rs");
let mut generated_file =
std::fs::File::create(&dest_path).expect("could not create generated code file");
let attribute_keys_len = attribute_keys.len();

let attribute_enum_keys = attribute_enum_values
.iter()
.map(|(enum_value, enum_alias)| {
let enum_value_ident = quote::format_ident!("{}", enum_value);
quote! {
#[serde(alias = #enum_alias)]
#enum_value_ident
}
});

let attribute_enum_values = attribute_enum_values
.keys()
.map(|k| quote::format_ident!("{}", k));
let attribute_const_values = generate_const_values_from_keys(attribute_keys.keys());
let metric_const_values = generate_const_values_from_keys(metric_keys.keys());

let tokens = quote! {
use schemars::JsonSchema;
use serde::Deserialize;

pub const ALL_ATTRS: &[TelemetryAttribute; #attribute_keys_len] = &[#(TelemetryAttribute::#attribute_enum_values),*];

#[derive(Debug, Deserialize, JsonSchema, Clone, Eq, PartialEq, Hash, Copy)]
pub enum TelemetryAttribute {
#(#attribute_enum_keys),*
}

#( #attribute_const_values )*

#( #metric_const_values )*
};

let file = parse2(tokens).expect("Could not parse TokenStream");
let code = prettyplease::unparse(&file);

write!(generated_file, "{}", code.to_string()).expect("Failed to write generated code");

// Inform cargo that we only want this to run when either this file or the telemetry
// one changes.
println!("cargo::rerun-if-changed=build.rs");
println!("cargo::rerun-if-changed=telemetry.toml");
}
17 changes: 12 additions & 5 deletions crates/apollo-mcp-server/src/graphql.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
//! Execute GraphQL operations from an MCP tool

use crate::generated::telemetry::{
APOLLO_MCP_ATTRIBUTE_OPERATION_ID, APOLLO_MCP_ATTRIBUTE_OPERATION_TYPE,
APOLLO_MCP_ATTRIBUTE_SUCCESS, APOLLO_MCP_METRIC_OPERATION_DURATION,
};
use crate::{errors::McpError, meter::get_meter};
use opentelemetry::KeyValue;
use reqwest::header::{HeaderMap, HeaderValue};
Expand Down Expand Up @@ -127,12 +131,15 @@ pub trait Executable {
// Record response metrics
let attributes = vec![
KeyValue::new(
"success",
APOLLO_MCP_ATTRIBUTE_SUCCESS,
result.as_ref().is_ok_and(|r| r.is_error != Some(true)),
),
KeyValue::new("operation.id", op_id.unwrap_or("unknown".to_string())),
KeyValue::new(
"operation.type",
APOLLO_MCP_ATTRIBUTE_OPERATION_ID,
op_id.unwrap_or("unknown".to_string()),
),
KeyValue::new(
APOLLO_MCP_ATTRIBUTE_OPERATION_TYPE,
if self.persisted_query_id().is_some() {
"persisted_query"
} else {
Expand All @@ -141,11 +148,11 @@ pub trait Executable {
),
];
meter
.f64_histogram("apollo.mcp.operation.duration")
.f64_histogram(APOLLO_MCP_METRIC_OPERATION_DURATION)
.build()
.record(start.elapsed().as_millis() as f64, &attributes);
meter
.u64_counter("apollo.mcp.operation.count")
.u64_counter(APOLLO_MCP_METRIC_OPERATION_DURATION)
.build()
.add(1, &attributes);

Expand Down
Loading
Loading