Skip to content

Commit 9b95428

Browse files
alocaySamuel Collard
authored andcommitted
feat: adding ability to omit attributes for traces and metrics (#358)
* feat: adding ability to omit attributes for traces and metrics * chore: adding changeset * chore: updating changeset text * chore: removing unused file * chore: renaming exporter file * chore: renaming module * re-running checks * chore: auto-gen the as_str function using enums instead of constants * chore: updating changeset entry and fixing fields in instruemnt attribute * chore: adding description as a doc string * chore: updating changeset entry * chore: updating operation attribute descriptions * chore: updating operation_type attribute to operation_source * chore: skipping request from instrumentation * chore: fixing clippy issues * re-running nix build * updating unit test snapshot * chore: fixing toml formatting issues * test: removing asserts on constants * chore: format issues * test: adjusting unit test to not use local envar * revert: undoing accidental commit * test: removing unnecessary assert
1 parent 9f764e7 commit 9b95428

File tree

12 files changed

+671
-27
lines changed

12 files changed

+671
-27
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
### feat: adding ability to omit attributes for traces and metrics - @alocay PR #358
2+
3+
Adding ability to configure which attributes are omitted from telemetry traces and metrics.
4+
5+
1. Using a Rust build script (`build.rs`) to auto-generate telemetry attribute code based on the data found in `telemetry.toml`.
6+
2. Utilizing an enum for attributes so typos in the config file raise an error.
7+
3. Omitting trace attributes by filtering it out in a custom exporter.
8+
4. Omitting metric attributes by indicating which attributes are allowed via a view.
9+
5. Created `telemetry_attributes.rs` to map `TelemetryAttribute` enum to a OTEL `Key`.
10+
11+
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:
12+
```
13+
[attributes.apollo.mcp]
14+
my_attribute = "Some attribute info"
15+
16+
[metrics.apollo.mcp]
17+
some.count = "Some metric count info"
18+
```
19+
This would generate a file that looks like the following:
20+
```
21+
/// All TelemetryAttribute values
22+
pub const ALL_ATTRS: &[TelemetryAttribute; 1usize] = &[
23+
TelemetryAttribute::MyAttribute
24+
];
25+
#[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema,, Clone, Eq, PartialEq, Hash, Copy)]
26+
pub enum TelemetryAttribute {
27+
///Some attribute info
28+
#[serde(alias = "my_attribute")]
29+
MyAttribute,
30+
}
31+
impl TelemetryAttribute {
32+
/// Supported telemetry attribute (tags) values
33+
pub const fn as_str(&self) -> &'static str {
34+
match self {
35+
TelemetryAttribute::MyAttribute => "apollo.mcp.my_attribute",
36+
}
37+
}
38+
}
39+
#[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema,, Clone, Eq, PartialEq, Hash, Copy)]
40+
pub enum TelemetryMetric {
41+
///Some metric count info
42+
#[serde(alias = "some.count")]
43+
SomeCount,
44+
}
45+
impl TelemetryMetric {
46+
/// Converts TelemetryMetric to &str
47+
pub const fn as_str(&self) -> &'static str {
48+
match self {
49+
TelemetryMetric::SomeCount => "apollo.mcp.some.count",
50+
}
51+
}
52+
}
53+
```
54+
An example configuration that omits `tool_name` attribute for metrics and `request_id` for tracing would look like the following:
55+
```
56+
telemetry:
57+
exporters:
58+
metrics:
59+
otlp:
60+
endpoint: "http://localhost:4317"
61+
protocol: "grpc"
62+
omitted_attributes:
63+
- tool_name
64+
tracing:
65+
otlp:
66+
endpoint: "http://localhost:4317"
67+
protocol: "grpc"
68+
omitted_attributes:
69+
- request_id
70+
```

Cargo.lock

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/apollo-mcp-server/Cargo.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ license-file.workspace = true
66
repository.workspace = true
77
rust-version.workspace = true
88
version.workspace = true
9+
build = "build.rs"
910

1011
default-run = "apollo-mcp-server"
1112

@@ -42,7 +43,9 @@ opentelemetry-otlp = { version = "0.30.0", features = [
4243
opentelemetry-resource-detectors = "0.9.0"
4344
opentelemetry-semantic-conventions = "0.30.0"
4445
opentelemetry-stdout = "0.30.0"
45-
opentelemetry_sdk = "0.30.0"
46+
opentelemetry_sdk = { version = "0.30.0", features = [
47+
"spec_unstable_metrics_views",
48+
] }
4649
regex = "1.11.1"
4750
reqwest-middleware = "0.4.2"
4851
reqwest-tracing = { version = "0.5.8", features = ["opentelemetry_0_30"] }
@@ -66,6 +69,7 @@ tracing-opentelemetry = "0.31.0"
6669
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
6770
tracing.workspace = true
6871
url.workspace = true
72+
async-trait = "0.1.89"
6973

7074
[dev-dependencies]
7175
chrono = { version = "0.4.41", default-features = false, features = ["now"] }
@@ -77,6 +81,14 @@ rstest.workspace = true
7781
tokio.workspace = true
7882
tracing-test = "0.2.5"
7983

84+
[build-dependencies]
85+
cruet = "0.15.0"
86+
prettyplease = "0.2.37"
87+
quote = "1.0.40"
88+
serde.workspace = true
89+
syn = "2.0.106"
90+
toml = "0.9.5"
91+
8092
[lints]
8193
workspace = true
8294

crates/apollo-mcp-server/build.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#![allow(clippy::unwrap_used)]
2+
#![allow(clippy::expect_used)]
3+
#![allow(clippy::panic)]
4+
5+
//! Build Script for the Apollo MCP Server
6+
//!
7+
//! This mostly compiles all the available telemetry attributes
8+
use quote::__private::TokenStream;
9+
use quote::quote;
10+
use serde::Deserialize;
11+
use std::io::Write;
12+
use std::{collections::VecDeque, io::Read as _};
13+
use syn::{Ident, parse2};
14+
15+
#[derive(Deserialize)]
16+
struct TelemetryTomlData {
17+
attributes: toml::Table,
18+
metrics: toml::Table,
19+
}
20+
21+
#[derive(Eq, PartialEq, Debug, Clone)]
22+
struct TelemetryData {
23+
name: String,
24+
alias: String,
25+
value: String,
26+
description: String,
27+
}
28+
29+
fn flatten(table: toml::Table) -> Vec<TelemetryData> {
30+
let mut to_visit = VecDeque::from_iter(table.into_iter().map(|(key, val)| (vec![key], val)));
31+
let mut telemetry_data = Vec::new();
32+
33+
while let Some((key, value)) = to_visit.pop_front() {
34+
match value {
35+
toml::Value::String(val) => {
36+
let last_key = key.last().unwrap().clone();
37+
telemetry_data.push(TelemetryData {
38+
name: cruet::to_pascal_case(last_key.as_str()),
39+
alias: last_key,
40+
value: key.join("."),
41+
description: val,
42+
});
43+
}
44+
toml::Value::Table(map) => to_visit.extend(
45+
map.into_iter()
46+
.map(|(nested_key, value)| ([key.clone(), vec![nested_key]].concat(), value)),
47+
),
48+
49+
_ => panic!("telemetry values should be string descriptions"),
50+
};
51+
}
52+
53+
telemetry_data
54+
}
55+
56+
fn generate_enum(telemetry_data: &[TelemetryData]) -> Vec<TokenStream> {
57+
telemetry_data
58+
.iter()
59+
.map(|t| {
60+
let enum_value_ident = quote::format_ident!("{}", &t.name);
61+
let alias = &t.alias;
62+
let doc_message = &t.description;
63+
quote! {
64+
#[doc = #doc_message]
65+
#[serde(alias = #alias)]
66+
#enum_value_ident
67+
}
68+
})
69+
.collect::<Vec<_>>()
70+
}
71+
72+
fn generate_enum_as_str_matches(
73+
telemetry_data: &[TelemetryData],
74+
enum_ident: Ident,
75+
) -> Vec<TokenStream> {
76+
telemetry_data
77+
.iter()
78+
.map(|t| {
79+
let name_ident = quote::format_ident!("{}", &t.name);
80+
let value = &t.value;
81+
quote! {
82+
#enum_ident::#name_ident => #value
83+
}
84+
})
85+
.collect::<Vec<_>>()
86+
}
87+
88+
fn main() {
89+
// Parse the telemetry file
90+
let telemetry: TelemetryTomlData = {
91+
let mut raw = String::new();
92+
std::fs::File::open("telemetry.toml")
93+
.expect("could not open telemetry file")
94+
.read_to_string(&mut raw)
95+
.expect("could not read telemetry file");
96+
97+
toml::from_str(&raw).expect("could not parse telemetry file")
98+
};
99+
100+
// Generate the keys
101+
let telemetry_attribute_data = flatten(telemetry.attributes);
102+
let telemetry_metrics_data = flatten(telemetry.metrics);
103+
println!(
104+
"a {:?} | m {:?}",
105+
telemetry_attribute_data, telemetry_metrics_data
106+
);
107+
108+
// Write out the generated keys
109+
let out_dir = std::env::var_os("OUT_DIR").expect("could not retrieve output directory");
110+
let dest_path = std::path::Path::new(&out_dir).join("telemetry_attributes.rs");
111+
let mut generated_file =
112+
std::fs::File::create(&dest_path).expect("could not create generated code file");
113+
114+
let attribute_keys_len = telemetry_attribute_data.len();
115+
let attribute_enum_keys = generate_enum(&telemetry_attribute_data);
116+
let all_attribute_enum_values = &telemetry_attribute_data
117+
.iter()
118+
.map(|t| quote::format_ident!("{}", t.name));
119+
let all_attribute_enum_values = (*all_attribute_enum_values).clone();
120+
let attribute_enum_name = quote::format_ident!("{}", "TelemetryAttribute");
121+
let attribute_enum_as_str_matches =
122+
generate_enum_as_str_matches(&telemetry_attribute_data, attribute_enum_name.clone());
123+
124+
let metric_enum_name = quote::format_ident!("{}", "TelemetryMetric");
125+
let metric_enum_keys = generate_enum(&telemetry_metrics_data);
126+
let metric_enum_as_str_matches =
127+
generate_enum_as_str_matches(&telemetry_metrics_data, metric_enum_name.clone());
128+
129+
let tokens = quote! {
130+
/// All TelemetryAttribute values
131+
pub const ALL_ATTRS: &[TelemetryAttribute; #attribute_keys_len] = &[#(TelemetryAttribute::#all_attribute_enum_values),*];
132+
133+
/// Supported telemetry attribute (tags) values
134+
#[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema, Clone, Eq, PartialEq, Hash, Copy)]
135+
pub enum #attribute_enum_name {
136+
#(#attribute_enum_keys),*
137+
}
138+
139+
impl #attribute_enum_name {
140+
/// Converts TelemetryAttribute to &str
141+
pub const fn as_str(&self) -> &'static str {
142+
match self {
143+
#(#attribute_enum_as_str_matches),*
144+
}
145+
}
146+
}
147+
148+
/// Supported telemetry metrics
149+
#[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema, Clone, Eq, PartialEq, Hash, Copy)]
150+
pub enum #metric_enum_name {
151+
#(#metric_enum_keys),*
152+
}
153+
154+
impl #metric_enum_name {
155+
/// Converts TelemetryMetric to &str
156+
pub const fn as_str(&self) -> &'static str {
157+
match self {
158+
#(#metric_enum_as_str_matches),*
159+
}
160+
}
161+
}
162+
};
163+
164+
let file = parse2(tokens).expect("Could not parse TokenStream");
165+
let code = prettyplease::unparse(&file);
166+
167+
write!(generated_file, "{}", code).expect("Failed to write generated code");
168+
169+
// Inform cargo that we only want this to run when either this file or the telemetry
170+
// one changes.
171+
println!("cargo::rerun-if-changed=build.rs");
172+
println!("cargo::rerun-if-changed=telemetry.toml");
173+
}

0 commit comments

Comments
 (0)