diff --git a/.changesets/config_telemetry_config.md b/.changesets/config_telemetry_config.md new file mode 100644 index 00000000..2f1132cc --- /dev/null +++ b/.changesets/config_telemetry_config.md @@ -0,0 +1,7 @@ +### Add basic config file options to otel telemetry - @swcollard PR #330 + +Adds new Configuration options for setting up configuration beyond the standard OTEL environment variables needed before. + +* Renames trace->telemetry +* Adds OTLP options for metrics and tracing to choose grpc or http upload protocols and setting the endpoints +* This configuration is all optional, so by default nothing will be logged \ No newline at end of file diff --git a/.changesets/feat_allow_attribute_configuration_alocay.md b/.changesets/feat_allow_attribute_configuration_alocay.md new file mode 100644 index 00000000..29ddfdba --- /dev/null +++ b/.changesets/feat_allow_attribute_configuration_alocay.md @@ -0,0 +1,70 @@ +### 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: +``` +[attributes.apollo.mcp] +my_attribute = "Some attribute info" + +[metrics.apollo.mcp] +some.count = "Some metric count info" +``` +This would generate a file that looks like the following: +``` +/// All TelemetryAttribute values +pub const ALL_ATTRS: &[TelemetryAttribute; 1usize] = &[ + TelemetryAttribute::MyAttribute +]; +#[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema,, Clone, Eq, PartialEq, Hash, Copy)] +pub enum TelemetryAttribute { + ///Some attribute info + #[serde(alias = "my_attribute")] + MyAttribute, +} +impl TelemetryAttribute { + /// Supported telemetry attribute (tags) values + pub const fn as_str(&self) -> &'static str { + match self { + TelemetryAttribute::MyAttribute => "apollo.mcp.my_attribute", + } + } +} +#[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema,, Clone, Eq, PartialEq, Hash, Copy)] +pub enum TelemetryMetric { + ///Some metric count info + #[serde(alias = "some.count")] + SomeCount, +} +impl TelemetryMetric { + /// Converts TelemetryMetric to &str + pub const fn as_str(&self) -> &'static str { + match self { + TelemetryMetric::SomeCount => "apollo.mcp.some.count", + } + } +} +``` +An example configuration that omits `tool_name` attribute for metrics and `request_id` for tracing 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 +``` diff --git a/.changesets/feat_otel_metrics.md b/.changesets/feat_otel_metrics.md new file mode 100644 index 00000000..8d553052 --- /dev/null +++ b/.changesets/feat_otel_metrics.md @@ -0,0 +1,11 @@ +### Implement metrics for mcp tool and operation counts and durations - @swcollard PR #297 + +This PR adds metrics to count and measure request duration to events throughout the MCP server + +* apollo.mcp.operation.duration +* apollo.mcp.operation.count +* apollo.mcp.tool.duration +* apollo.mcp.tool.count +* apollo.mcp.initialize.count +* apollo.mcp.list_tools.count +* apollo.mcp.get_info.count diff --git a/.changesets/feat_otel_prototype.md b/.changesets/feat_otel_prototype.md new file mode 100644 index 00000000..3a530264 --- /dev/null +++ b/.changesets/feat_otel_prototype.md @@ -0,0 +1,9 @@ +### Prototype OpenTelemetry Traces in MCP Server - @swcollard PR #274 + +Pulls in new crates and SDKs for prototyping instrumenting the Apollo MCP Server with Open Telemetry Traces. + +* Adds new rust crates to support OTel +* Annotates excecute and call_tool functions with trace macro +* Adds Axum and Tower middleware's for OTel tracing +* Refactors Logging so that all the tracing_subscribers are set together in a single module. + diff --git a/.changesets/feat_telemetry_operations_trace_metrics.md b/.changesets/feat_telemetry_operations_trace_metrics.md new file mode 100644 index 00000000..56ed75fe --- /dev/null +++ b/.changesets/feat_telemetry_operations_trace_metrics.md @@ -0,0 +1,4 @@ +### Telemetry: Trace operations and auth - @swcollard PR #375 + +* Adds traces for the MCP server generating Tools from Operations and performing authorization +* Includes the HTTP status code to the top level HTTP trace \ No newline at end of file diff --git a/.changesets/feat_trace_sampling_option_alocay.md b/.changesets/feat_trace_sampling_option_alocay.md new file mode 100644 index 00000000..38bec085 --- /dev/null +++ b/.changesets/feat_trace_sampling_option_alocay.md @@ -0,0 +1,8 @@ +### feat: adding config option for trace sampling - @alocay PR #366 + +Adding configuration option to sample traces. Can use the following options: +1. Ratio based samples (ratio >= 1 is always sample) +2. Always on +3. Always off + +Defaults to always on if not provided. diff --git a/.changesets/fix_telemetry_fix_downstream_traces.md b/.changesets/fix_telemetry_fix_downstream_traces.md new file mode 100644 index 00000000..d5a8febf --- /dev/null +++ b/.changesets/fix_telemetry_fix_downstream_traces.md @@ -0,0 +1,3 @@ +### fix: Include the cargo feature and TraceContextPropagator to send otel headers downstream - @swcollard PR #307 + +Inside the reqwest middleware, if the global text_map_propagator is not set, it will no op and not send the traceparent and tracestate headers to the Router. Adding this is needed to correlate traces from the mcp server to the router or other downstream APIs \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 56dc3b75..9773eb4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,12 +46,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -113,15 +107,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "apollo-compiler" -version = "1.29.0" +version = "1.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4369d2ac382b0752cc5ff8cdb020e7a3c74480e7d940fc99f139281f8701fb81" +checksum = "87e4c0116cde9e3e5679806cf91c464d9efb7f1e231abffc505e0f6d4b911260" dependencies = [ "ahash", "apollo-parser", @@ -131,22 +125,24 @@ dependencies = [ "rowan", "serde", "serde_json_bytes", - "thiserror 2.0.14", + "thiserror 2.0.16", "triomphe", "typed-arena", ] [[package]] name = "apollo-federation" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbea4f0fcfcd95ec6f03c41a104488f66eafb37d05bd304a219702d1814449c5" +checksum = "1595bfb0fd31882d0b2dd258205ccac93a43c0ae37038a1a6e1cc2834eaf958f" dependencies = [ "apollo-compiler", + "countmap", "derive_more", "either", + "encoding_rs", "form_urlencoded", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "http", "indexmap", "itertools", @@ -167,7 +163,7 @@ dependencies = [ "shape", "strum", "strum_macros", - "thiserror 2.0.14", + "thiserror 2.0.16", "time", "tracing", "url", @@ -188,7 +184,7 @@ dependencies = [ "serde", "serde_json", "test-log", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-stream", "tower", @@ -210,11 +206,15 @@ dependencies = [ "apollo-federation", "apollo-mcp-registry", "apollo-schema-index", + "async-trait", "axum", "axum-extra", + "axum-otel-metrics", + "axum-tracing-opentelemetry", "bon", "chrono", "clap", + "cruet", "figment", "futures", "headers", @@ -226,20 +226,35 @@ dependencies = [ "jwks", "lz-str", "mockito", + "opentelemetry", + "opentelemetry-appender-log", + "opentelemetry-otlp", + "opentelemetry-resource-detectors", + "opentelemetry-semantic-conventions", + "opentelemetry-stdout", + "opentelemetry_sdk", + "prettyplease", + "quote", "regex", "reqwest", + "reqwest-middleware", + "reqwest-tracing", "rmcp", "rstest", "schemars", "serde", "serde_json", - "thiserror 2.0.14", + "syn 2.0.106", + "thiserror 2.0.16", "tokio", "tokio-util", + "toml", "tower", "tower-http", "tracing", "tracing-appender", + "tracing-core", + "tracing-opentelemetry", "tracing-subscriber", "tracing-test", "url", @@ -266,7 +281,7 @@ dependencies = [ "itertools", "rstest", "tantivy", - "thiserror 2.0.14", + "thiserror 2.0.16", "tracing", ] @@ -299,13 +314,13 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.27" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" +checksum = "977eb15ea9efd848bb8a4a1a2500347ed7f0bf794edf0dc3ddcf439f43d36b23" dependencies = [ - "flate2", + "compression-codecs", + "compression-core", "futures-core", - "memchr", "pin-project-lite", "tokio", ] @@ -318,7 +333,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -419,6 +434,41 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-otel-metrics" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cf7343b4fc88312e4d7b731152a1a09edcdb6def398d836ef8bccb57f066a" +dependencies = [ + "axum", + "futures-util", + "http", + "http-body", + "opentelemetry", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "pin-project-lite", + "tower", +] + +[[package]] +name = "axum-tracing-opentelemetry" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd3e188039e0e9e3dce1ad873358fd6bab72e6496e18b898bc36d72c07af4b26" +dependencies = [ + "axum", + "futures-core", + "futures-util", + "http", + "opentelemetry", + "pin-project-lite", + "tower", + "tracing", + "tracing-opentelemetry", + "tracing-opentelemetry-instrumentation-sdk", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -463,9 +513,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bitpacking" @@ -487,9 +537,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.7.0" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a0c21249ad725ebcadcb1b1885f8e3d56e8e6b8924f560268aab000982d637" +checksum = "c2529c31017402be841eb45892278a6c21a000c0a17643af326c73a73f83f0fb" dependencies = [ "bon-macros", "rustversion", @@ -497,17 +547,17 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.7.0" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a660ebdea4d4d3ec7788cfc9c035b66efb66028b9b97bf6cde7023ccc8e77e28" +checksum = "d82020dadcb845a345591863adb65d74fa8dc5c18a0b6d408470e13b7adc7005" dependencies = [ - "darling 0.21.2", + "darling", "ident_case", "prettyplease", "proc-macro2", "quote", "rustversion", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -558,10 +608,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.32" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -575,30 +626,29 @@ checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", - "num-traits", + "num-traits 0.2.19", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.0", ] [[package]] name = "clap" -version = "4.5.45" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", @@ -606,9 +656,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -618,14 +668,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -659,6 +709,23 @@ dependencies = [ "memchr", ] +[[package]] +name = "compression-codecs" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "485abf41ac0c8047c07c87c72c8fb3eb5197f6e9d7ded615dfd1a00ae00a0f64" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + [[package]] name = "concolor" version = "0.1.1" @@ -716,6 +783,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "countmap" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef2a403c4af585607826502480ab6e453f320c230ef67255eee21f0cc72c0a6" +dependencies = [ + "num-traits 0.1.43", +] + [[package]] name = "countme" version = "3.0.1" @@ -774,6 +850,16 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "cruet" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a9ae414b9768aada1b316493261653e41af05c9d2ccc9c504a8fc051c6a790" +dependencies = [ + "once_cell", + "regex", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -792,81 +878,47 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08440b3dd222c3d0433e63e097463969485f112baff337dfdaca043a0d760570" -dependencies = [ - "darling_core 0.21.2", - "darling_macro 0.21.2", -] - -[[package]] -name = "darling_core" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "syn 2.0.105", + "darling_core", + "darling_macro", ] [[package]] name = "darling_core" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25b7912bc28a04ab1b7715a68ea03aaa15662b43a1a4b2c480531fd19f8bf7e" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.105", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] name = "darling_macro" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce154b9bea7fb0c8e8326e62d00354000c36e79770ff21b8c84e3aa267d9d531" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core 0.21.2", + "darling_core", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] name = "deadpool" -version = "0.10.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" dependencies = [ - "async-trait", "deadpool-runtime", + "lazy_static", "num_cpus", "tokio", ] @@ -879,12 +931,12 @@ checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -905,7 +957,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "unicode-xid", ] @@ -927,14 +979,14 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] name = "downcast-rs" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" [[package]] name = "dyn-clone" @@ -951,7 +1003,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -975,6 +1027,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum-ordinalize" version = "4.3.0" @@ -992,28 +1053,28 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] name = "enumset" -version = "1.1.7" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ee17054f550fd7400e1906e2f9356c7672643ed34008a9e8abe147ccd2d821" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" dependencies = [ "enumset_derive", ] [[package]] name = "enumset_derive" -version = "0.12.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d07902c93376f1e96c34abc4d507c0911df3816cef50b01f5a2ff3ad8c370d" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" dependencies = [ - "darling 0.20.11", + "darling", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -1024,19 +1085,19 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] name = "fancy-regex" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf04c5ec15464ace8355a7b440a33aece288993475556d461154d7a62ad9947c" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" dependencies = [ "bit-set", "regex-automata", @@ -1071,6 +1132,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + [[package]] name = "fixedbitset" version = "0.5.7" @@ -1110,6 +1177,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1127,9 +1200,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -1220,7 +1293,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -1291,7 +1364,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] @@ -1410,7 +1483,18 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] @@ -1509,9 +1593,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "humantime-serde" @@ -1525,13 +1609,14 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -1539,11 +1624,25 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1562,9 +1661,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64", "bytes", @@ -1595,9 +1694,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1711,9 +1810,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1732,13 +1831,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde", + "serde_core", ] [[package]] @@ -1753,7 +1853,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "inotify-sys", "libc", ] @@ -1769,9 +1869,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.43.1" +version = "1.43.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" +checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" dependencies = [ "console", "globset", @@ -1785,11 +1885,11 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -1844,9 +1944,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -1854,9 +1954,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -1890,7 +1990,7 @@ dependencies = [ "idna", "itoa", "num-cmp", - "num-traits", + "num-traits 0.2.19", "once_cell", "percent-encoding", "referencing", @@ -1971,9 +2071,9 @@ checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libm" @@ -1995,9 +2095,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -2017,9 +2117,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru" @@ -2074,9 +2174,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", ] @@ -2203,7 +2303,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "fsevent-sys", "inotify", "kqueue", @@ -2241,7 +2341,7 @@ dependencies = [ "num-integer", "num-iter", "num-rational", - "num-traits", + "num-traits 0.2.19", ] [[package]] @@ -2251,7 +2351,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", - "num-traits", + "num-traits 0.2.19", ] [[package]] @@ -2266,7 +2366,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ - "num-traits", + "num-traits 0.2.19", ] [[package]] @@ -2281,7 +2381,7 @@ version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "num-traits", + "num-traits 0.2.19", ] [[package]] @@ -2292,7 +2392,7 @@ checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", - "num-traits", + "num-traits 0.2.19", ] [[package]] @@ -2303,7 +2403,16 @@ checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ "num-bigint", "num-integer", - "num-traits", + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.19", ] [[package]] @@ -2368,7 +2477,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -2385,7 +2494,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -2416,6 +2525,120 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "opentelemetry-appender-log" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e688026e48f4603494f619583e0aa0b0edd9c0b9430e1c46804df2ff32bc8798" +dependencies = [ + "log", + "opentelemetry", +] + +[[package]] +name = "opentelemetry-http" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" +dependencies = [ + "http", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "thiserror 2.0.16", + "tokio", + "tonic", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", +] + +[[package]] +name = "opentelemetry-resource-detectors" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a44e076f07fa3d76e741991f4f7d3ecbac0eed8521ced491fbdf8db77d024cf" +dependencies = [ + "opentelemetry", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d059a296a47436748557a353c5e6c5705b9470ef6c95cfc52c21a8814ddac2" + +[[package]] +name = "opentelemetry-stdout" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447191061af41c3943e082ea359ab8b64ff27d6d34d30d327df309ddef1eef6f" +dependencies = [ + "chrono", + "opentelemetry", + "opentelemetry_sdk", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand 0.9.2", + "serde_json", + "thiserror 2.0.16", + "tokio", + "tokio-stream", +] + [[package]] name = "outref" version = "0.5.2" @@ -2480,7 +2703,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -2495,26 +2718,26 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror 2.0.14", + "thiserror 2.0.16", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" dependencies = [ "pest", "pest_generator", @@ -2522,22 +2745,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] name = "pest_meta" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" dependencies = [ "pest", "sha2", @@ -2573,7 +2796,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -2596,9 +2819,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -2620,28 +2843,28 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.97" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -2654,11 +2877,34 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "version_check", "yansi", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "quote" version = "1.0.40" @@ -2739,7 +2985,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ - "num-traits", + "num-traits 0.2.19", "rand 0.8.5", ] @@ -2769,7 +3015,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", ] [[package]] @@ -2789,7 +3035,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -2808,9 +3054,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -2820,9 +3066,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", @@ -2831,9 +3077,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "relative-path" @@ -2881,6 +3127,39 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reqwest-middleware" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" +dependencies = [ + "anyhow", + "async-trait", + "http", + "reqwest", + "serde", + "thiserror 1.0.69", + "tower-service", +] + +[[package]] +name = "reqwest-tracing" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70ea85f131b2ee9874f0b160ac5976f8af75f3c9badfe0d955880257d10bd83" +dependencies = [ + "anyhow", + "async-trait", + "getrandom 0.2.16", + "http", + "matchit", + "opentelemetry", + "reqwest", + "reqwest-middleware", + "tracing", + "tracing-opentelemetry", +] + [[package]] name = "ring" version = "0.17.14" @@ -2917,7 +3196,7 @@ dependencies = [ "serde", "serde_json", "sse-stream", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-stream", "tokio-util", @@ -2932,11 +3211,11 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1827cd98dab34cade0513243c6fe0351f0f0b2c9d6825460bcf45b42804bdda0" dependencies = [ - "darling 0.21.2", + "darling", "proc-macro2", "quote", "serde_json", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -2977,7 +3256,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.105", + "syn 2.0.106", "unicode-ident", ] @@ -3024,7 +3303,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3033,15 +3312,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.0", ] [[package]] @@ -3076,11 +3355,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -3107,7 +3386,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -3132,7 +3411,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation", "core-foundation-sys", "libc", @@ -3141,9 +3420,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -3151,28 +3430,38 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -3183,20 +3472,21 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "indexmap", "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -3216,12 +3506,22 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +dependencies = [ + "serde_core", ] [[package]] @@ -3273,9 +3573,9 @@ dependencies = [ [[package]] name = "shape" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "362c1523018b16b65737aa0ea76a731edbcd399e273c0130ba829b148f89dbd2" +checksum = "914e2afe9130bf8acf52c5e20b4222f7d2e5eb8327e05fb668fe70aad4b3a896" dependencies = [ "apollo-compiler", "indexmap", @@ -3320,8 +3620,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", - "num-traits", - "thiserror 2.0.14", + "num-traits 0.2.19", + "thiserror 2.0.16", "time", ] @@ -3396,7 +3696,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -3412,9 +3712,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.105" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -3438,7 +3738,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -3487,7 +3787,7 @@ dependencies = [ "tantivy-stacker", "tantivy-tokenizer-api", "tempfile", - "thiserror 2.0.14", + "thiserror 2.0.16", "time", "uuid", "winapi", @@ -3589,15 +3889,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.8", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.61.0", ] [[package]] @@ -3618,7 +3918,7 @@ checksum = "451b374529930d7601b1eef8d32bc79ae870b6079b069401709c2a8bf9e75f36" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -3638,11 +3938,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.14", + "thiserror-impl 2.0.16", ] [[package]] @@ -3653,18 +3953,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -3678,9 +3978,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -3695,15 +3995,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -3747,7 +4047,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -3784,23 +4084,83 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.23.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" dependencies = [ "indexmap", "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" + +[[package]] +name = "tonic" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +dependencies = [ + "async-trait", + "base64", + "bytes", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -3809,9 +4169,12 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -3823,7 +4186,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "bytes", "futures-util", "http", @@ -3833,6 +4196,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -3879,7 +4243,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -3915,6 +4279,36 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + +[[package]] +name = "tracing-opentelemetry-instrumentation-sdk" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13836788f587ab71400ef44b07196430782b5e483e189933c38dddb81381574" +dependencies = [ + "http", + "opentelemetry", + "tracing", + "tracing-opentelemetry", +] + [[package]] name = "tracing-serde" version = "0.2.0" @@ -3964,7 +4358,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4012,9 +4406,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -4048,9 +4442,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -4078,9 +4472,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", @@ -4150,44 +4544,54 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -4198,9 +4602,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4208,31 +4612,41 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -4256,11 +4670,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -4271,13 +4685,13 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.2.0", "windows-result", "windows-strings", ] @@ -4290,7 +4704,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4301,7 +4715,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4310,22 +4724,28 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ - "windows-link", + "windows-link 0.2.0", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -4364,6 +4784,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -4401,7 +4830,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -4552,21 +4981,20 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] name = "wiremock" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b8b99d4cdbf36b239a9532e31fe4fb8acc38d1897c1761e161550a7dc78e6a" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" dependencies = [ "assert-json-diff", - "async-trait", "base64", "deadpool", "futures", @@ -4584,13 +5012,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -4624,28 +5049,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4665,7 +5090,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "synstructure", ] @@ -4705,7 +5130,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -4728,9 +5153,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index c3c09c07..6364840a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,12 @@ url = { version = "2.4", features = ["serde"] } [workspace.metadata] crane.name = "apollo-mcp" +# This allows usage of coverage(off) attribute without causing a linting error. +# This attribute doesn't work in stable Rust yet and can be removed whenever it does. +# See https://github.com/apollographql/apollo-mcp-server/pull/372 +[workspace.lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } + [workspace.lints.clippy] exit = "deny" expect_used = "deny" diff --git a/crates/apollo-mcp-registry/src/platform_api/operation_collections/collection_poller.rs b/crates/apollo-mcp-registry/src/platform_api/operation_collections/collection_poller.rs index 9f1a6605..faeab984 100644 --- a/crates/apollo-mcp-registry/src/platform_api/operation_collections/collection_poller.rs +++ b/crates/apollo-mcp-registry/src/platform_api/operation_collections/collection_poller.rs @@ -248,7 +248,7 @@ impl From<&OperationCollectionDefaultEntry> for OperationData { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum CollectionSource { Id(String, PlatformApiConfig), Default(String, PlatformApiConfig), diff --git a/crates/apollo-mcp-server/Cargo.toml b/crates/apollo-mcp-server/Cargo.toml index cb5903dc..b2b0fcbc 100644 --- a/crates/apollo-mcp-server/Cargo.toml +++ b/crates/apollo-mcp-server/Cargo.toml @@ -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" @@ -17,6 +18,8 @@ apollo-mcp-registry = { path = "../apollo-mcp-registry" } apollo-schema-index = { path = "../apollo-schema-index" } axum = "0.8.4" axum-extra = { version = "0.10.1", features = ["typed-header"] } +axum-otel-metrics = "0.12.0" +axum-tracing-opentelemetry = "0.29.0" bon = "3.6.3" clap = { version = "4.5.36", features = ["derive", "env"] } figment = { version = "0.10.19", features = ["env", "yaml"] } @@ -28,7 +31,24 @@ jsonschema = "0.33.0" jsonwebtoken = "9" jwks = "0.4.0" lz-str = "0.2.1" +opentelemetry = "0.30.0" +opentelemetry-appender-log = "0.30.0" +opentelemetry-otlp = { version = "0.30.0", features = [ + "grpc-tonic", + "tonic", + "http-proto", + "metrics", + "trace", +] } +opentelemetry-resource-detectors = "0.9.0" +opentelemetry-semantic-conventions = "0.30.0" +opentelemetry-stdout = "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"] } reqwest.workspace = true rmcp = { version = "0.6", features = [ "server", @@ -41,23 +61,35 @@ serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio.workspace = true -tracing.workspace = true +tokio-util = "0.7.15" +tower-http = { version = "0.6.6", features = ["cors", "trace"] } tracing-appender = "0.2.3" +tracing-core.workspace = true +tracing-opentelemetry = "0.31.0" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } -tokio-util = "0.7.15" -tower-http = { version = "0.6.6", features = ["cors"] } +tracing.workspace = true url.workspace = true +async-trait = "0.1.89" [dev-dependencies] chrono = { version = "0.4.41", default-features = false, features = ["now"] } figment = { version = "0.10.19", features = ["test"] } insta.workspace = true mockito = "1.7.0" +opentelemetry_sdk = { version = "0.30.0", features = ["testing"] } rstest.workspace = true tokio.workspace = true tower = "0.5.2" tracing-test = "0.2.5" +[build-dependencies] +cruet = "0.15.0" +prettyplease = "0.2.37" +quote = "1.0.40" +serde.workspace = true +syn = "2.0.106" +toml = "0.9.5" + [lints] workspace = true diff --git a/crates/apollo-mcp-server/build.rs b/crates/apollo-mcp-server/build.rs new file mode 100644 index 00000000..f213cc35 --- /dev/null +++ b/crates/apollo-mcp-server/build.rs @@ -0,0 +1,169 @@ +#![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 serde::Deserialize; +use std::io::Write; +use std::{collections::VecDeque, io::Read as _}; +use syn::{Ident, parse2}; + +#[derive(Deserialize)] +struct TelemetryTomlData { + attributes: toml::Table, + metrics: toml::Table, +} + +#[derive(Eq, PartialEq, Debug, Clone)] +struct TelemetryData { + name: String, + alias: String, + value: String, + description: String, +} + +fn flatten(table: toml::Table) -> Vec { + let mut to_visit = VecDeque::from_iter(table.into_iter().map(|(key, val)| (vec![key], val))); + let mut telemetry_data = Vec::new(); + + while let Some((key, value)) = to_visit.pop_front() { + match value { + toml::Value::String(val) => { + let last_key = key.last().unwrap().clone(); + telemetry_data.push(TelemetryData { + name: cruet::to_pascal_case(last_key.as_str()), + alias: last_key, + value: key.join("."), + description: 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"), + }; + } + + telemetry_data +} + +fn generate_enum(telemetry_data: &[TelemetryData]) -> Vec { + telemetry_data + .iter() + .map(|t| { + let enum_value_ident = quote::format_ident!("{}", &t.name); + let alias = &t.alias; + let doc_message = &t.description; + quote! { + #[doc = #doc_message] + #[serde(alias = #alias)] + #enum_value_ident + } + }) + .collect::>() +} + +fn generate_enum_as_str_matches( + telemetry_data: &[TelemetryData], + enum_ident: Ident, +) -> Vec { + telemetry_data + .iter() + .map(|t| { + let name_ident = quote::format_ident!("{}", &t.name); + let value = &t.value; + quote! { + #enum_ident::#name_ident => #value + } + }) + .collect::>() +} + +fn main() { + // Parse the telemetry file + let telemetry: TelemetryTomlData = { + 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 telemetry_attribute_data = flatten(telemetry.attributes); + let telemetry_metrics_data = flatten(telemetry.metrics); + + // 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 = telemetry_attribute_data.len(); + let attribute_enum_keys = generate_enum(&telemetry_attribute_data); + let all_attribute_enum_values = &telemetry_attribute_data + .iter() + .map(|t| quote::format_ident!("{}", t.name)); + let all_attribute_enum_values = (*all_attribute_enum_values).clone(); + let attribute_enum_name = quote::format_ident!("{}", "TelemetryAttribute"); + let attribute_enum_as_str_matches = + generate_enum_as_str_matches(&telemetry_attribute_data, attribute_enum_name.clone()); + + let metric_enum_name = quote::format_ident!("{}", "TelemetryMetric"); + let metric_enum_keys = generate_enum(&telemetry_metrics_data); + let metric_enum_as_str_matches = + generate_enum_as_str_matches(&telemetry_metrics_data, metric_enum_name.clone()); + + let tokens = quote! { + /// All TelemetryAttribute values + pub const ALL_ATTRS: &[TelemetryAttribute; #attribute_keys_len] = &[#(TelemetryAttribute::#all_attribute_enum_values),*]; + + /// Supported telemetry attribute (tags) values + #[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema, Clone, Eq, PartialEq, Hash, Copy)] + pub enum #attribute_enum_name { + #(#attribute_enum_keys),* + } + + impl #attribute_enum_name { + /// Converts TelemetryAttribute to &str + pub const fn as_str(&self) -> &'static str { + match self { + #(#attribute_enum_as_str_matches),* + } + } + } + + /// Supported telemetry metrics + #[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema, Clone, Eq, PartialEq, Hash, Copy)] + pub enum #metric_enum_name { + #(#metric_enum_keys),* + } + + impl #metric_enum_name { + /// Converts TelemetryMetric to &str + pub const fn as_str(&self) -> &'static str { + match self { + #(#metric_enum_as_str_matches),* + } + } + } + }; + + let file = parse2(tokens).expect("Could not parse TokenStream"); + let code = prettyplease::unparse(&file); + + write!(generated_file, "{}", code).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"); +} diff --git a/crates/apollo-mcp-server/src/auth.rs b/crates/apollo-mcp-server/src/auth.rs index 1c802828..e52e1729 100644 --- a/crates/apollo-mcp-server/src/auth.rs +++ b/crates/apollo-mcp-server/src/auth.rs @@ -84,6 +84,7 @@ impl Config { } /// Validate that requests made have a corresponding bearer JWT token +#[tracing::instrument(skip_all, fields(status_code, reason))] async fn oauth_validate( State(auth_config): State, token: Option>>, @@ -104,17 +105,85 @@ async fn oauth_validate( }; let validator = NetworkedTokenValidator::new(&auth_config.audiences, &auth_config.servers); - let token = token.ok_or_else(unauthorized_error)?; - - let valid_token = validator - .validate(token.0) - .await - .ok_or_else(unauthorized_error)?; + let token = token.ok_or_else(|| { + tracing::Span::current().record("reason", "missing_token"); + tracing::Span::current().record("status_code", StatusCode::UNAUTHORIZED.as_u16()); + unauthorized_error() + })?; + + let valid_token = validator.validate(token.0).await.ok_or_else(|| { + tracing::Span::current().record("reason", "invalid_token"); + tracing::Span::current().record("status_code", StatusCode::UNAUTHORIZED.as_u16()); + unauthorized_error() + })?; // Insert new context to ensure that handlers only use our enforced token verification // for propagation request.extensions_mut().insert(valid_token); let response = next.run(request).await; + tracing::Span::current().record("status_code", response.status().as_u16()); Ok(response) } + +#[cfg(test)] +mod tests { + use super::*; + use axum::middleware::from_fn_with_state; + use axum::routing::get; + use axum::{ + Router, + body::Body, + http::{Request, StatusCode}, + }; + use http::header::{AUTHORIZATION, WWW_AUTHENTICATE}; + use tower::ServiceExt; // for .oneshot() + use url::Url; + + fn test_config() -> Config { + Config { + servers: vec![Url::parse("http://localhost:1234").unwrap()], + audiences: vec!["test-audience".to_string()], + resource: Url::parse("http://localhost:4000").unwrap(), + resource_documentation: None, + scopes: vec!["read".to_string()], + disable_auth_token_passthrough: false, + } + } + + fn test_router(config: Config) -> Router { + Router::new() + .route("/test", get(|| async { "ok" })) + .layer(from_fn_with_state(config, oauth_validate)) + } + + #[tokio::test] + async fn missing_token_returns_unauthorized() { + let config = test_config(); + let app = test_router(config.clone()); + let req = Request::builder().uri("/test").body(Body::empty()).unwrap(); + let res = app.oneshot(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); + let headers = res.headers(); + let www_auth = headers.get(WWW_AUTHENTICATE).unwrap().to_str().unwrap(); + assert!(www_auth.contains("Bearer")); + assert!(www_auth.contains("resource_metadata")); + } + + #[tokio::test] + async fn invalid_token_returns_unauthorized() { + let config = test_config(); + let app = test_router(config.clone()); + let req = Request::builder() + .uri("/test") + .header(AUTHORIZATION, "Bearer invalidtoken") + .body(Body::empty()) + .unwrap(); + let res = app.oneshot(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); + let headers = res.headers(); + let www_auth = headers.get(WWW_AUTHENTICATE).unwrap().to_str().unwrap(); + assert!(www_auth.contains("Bearer")); + assert!(www_auth.contains("resource_metadata")); + } +} diff --git a/crates/apollo-mcp-server/src/graphql.rs b/crates/apollo-mcp-server/src/graphql.rs index d47e61e2..8ed86941 100644 --- a/crates/apollo-mcp-server/src/graphql.rs +++ b/crates/apollo-mcp-server/src/graphql.rs @@ -1,11 +1,17 @@ //! Execute GraphQL operations from an MCP tool use crate::errors::McpError; +use crate::generated::telemetry::{TelemetryAttribute, TelemetryMetric}; +use crate::meter; +use opentelemetry::KeyValue; use reqwest::header::{HeaderMap, HeaderValue}; +use reqwest_middleware::{ClientBuilder, Extension}; +use reqwest_tracing::{OtelName, TracingMiddleware}; use rmcp::model::{CallToolResult, Content, ErrorCode}; use serde_json::{Map, Value}; use url::Url; +#[derive(Debug)] pub struct Request<'a> { pub input: Value, pub endpoint: &'a Url, @@ -33,7 +39,11 @@ pub trait Executable { fn headers(&self, default_headers: &HeaderMap) -> HeaderMap; /// Execute as a GraphQL operation using the endpoint and headers + #[tracing::instrument(skip(self, request))] async fn execute(&self, request: Request<'_>) -> Result { + let meter = &meter::METER; + let start = std::time::Instant::now(); + let mut op_id: Option = None; let client_metadata = serde_json::json!({ "name": "mcp", "version": std::env!("CARGO_PKG_VERSION") @@ -55,6 +65,7 @@ pub trait Executable { "clientLibrary": client_metadata, }), ); + op_id = Some(id.to_string()); } else { let OperationDetails { query, @@ -70,11 +81,17 @@ pub trait Executable { ); if let Some(op_name) = operation_name { + op_id = Some(op_name.clone()); request_body.insert(String::from("operationName"), Value::String(op_name)); } } - reqwest::Client::new() + let client = ClientBuilder::new(reqwest::Client::new()) + .with_init(Extension(OtelName("mcp-graphql-client".into()))) + .with(TracingMiddleware::default()) + .build(); + + let result = client .post(request.endpoint.as_str()) .headers(self.headers(&request.headers)) .body(Value::Object(request_body).to_string()) @@ -109,15 +126,50 @@ pub trait Executable { ), meta: None, structured_content: Some(json), - }) + }); + + // Record response metrics + let attributes = vec![ + KeyValue::new( + TelemetryAttribute::Success.to_key(), + result.as_ref().is_ok_and(|r| r.is_error != Some(true)), + ), + KeyValue::new( + TelemetryAttribute::OperationId.to_key(), + op_id.unwrap_or("".to_string()), + ), + KeyValue::new( + TelemetryAttribute::OperationSource.to_key(), + match self.persisted_query_id() { + Some(_) => "persisted_query", + None => "operation", + }, + ), + ]; + meter + .f64_histogram(TelemetryMetric::OperationDuration.as_str()) + .build() + .record(start.elapsed().as_millis() as f64, &attributes); + meter + .u64_counter(TelemetryMetric::OperationCount.as_str()) + .build() + .add(1, &attributes); + + result } } #[cfg(test)] mod test { use crate::errors::McpError; + use crate::generated::telemetry::TelemetryMetric; use crate::graphql::{Executable, OperationDetails, Request}; use http::{HeaderMap, HeaderValue}; + use opentelemetry::global; + use opentelemetry_sdk::metrics::data::{AggregatedMetrics, MetricData}; + use opentelemetry_sdk::metrics::{ + InMemoryMetricExporter, MeterProviderBuilder, PeriodicReader, + }; use serde_json::{Map, Value, json}; use url::Url; @@ -357,4 +409,76 @@ mod test { assert!(result.is_error.is_some()); assert!(result.is_error.unwrap()); } + + #[tokio::test] + async fn validate_metric_attributes_success_false() { + // given + let exporter = InMemoryMetricExporter::default(); + let meter_provider = MeterProviderBuilder::default() + .with_reader(PeriodicReader::builder(exporter.clone()).build()) + .build(); + global::set_meter_provider(meter_provider.clone()); + + let mut server = mockito::Server::new_async().await; + let url = Url::parse(server.url().as_str()).unwrap(); + let mock_request = Request { + input: json!({}), + endpoint: &url, + headers: HeaderMap::new(), + }; + + server + .mock("POST", "/") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!({ "data": null, "errors": ["an error"] }).to_string()) + .expect(1) + .create_async() + .await; + + // when + let test_executable = TestExecutableWithPersistedQueryId {}; + let result = test_executable.execute(mock_request).await.unwrap(); + + // then + assert!(result.is_error.is_some()); + assert!(result.is_error.unwrap()); + + // Retrieve the finished metrics from the exporter + let finished_metrics = exporter.get_finished_metrics().unwrap(); + + // validate the attributes of the apollo.mcp.operation.count counter + for resource_metrics in finished_metrics { + if let Some(scope_metrics) = resource_metrics + .scope_metrics() + .find(|scope_metrics| scope_metrics.scope().name() == "apollo.mcp") + { + for metric in scope_metrics.metrics() { + if metric.name() == TelemetryMetric::OperationCount.as_str() + && let AggregatedMetrics::U64(MetricData::Sum(data)) = metric.data() + { + for point in data.data_points() { + let attributes = point.attributes(); + let mut attr_map = std::collections::HashMap::new(); + for kv in attributes { + attr_map.insert(kv.key.as_str(), kv.value.as_str()); + } + assert_eq!( + attr_map.get("operation.id").map(|s| s.as_ref()), + Some("mock_operation") + ); + assert_eq!( + attr_map.get("operation.type").map(|s| s.as_ref()), + Some("persisted_query") + ); + assert_eq!( + attr_map.get("success"), + Some(&std::borrow::Cow::Borrowed("false")) + ); + } + } + } + } + } + } } diff --git a/crates/apollo-mcp-server/src/introspection/tools/introspect.rs b/crates/apollo-mcp-server/src/introspection/tools/introspect.rs index 7d92996e..0bdcc2b3 100644 --- a/crates/apollo-mcp-server/src/introspection/tools/introspect.rs +++ b/crates/apollo-mcp-server/src/introspection/tools/introspect.rs @@ -27,7 +27,7 @@ pub struct Introspect { } /// Input for the introspect tool. -#[derive(JsonSchema, Deserialize)] +#[derive(JsonSchema, Deserialize, Debug)] pub struct Input { /// The name of the type to get information about. type_name: String, @@ -55,6 +55,7 @@ impl Introspect { } } + #[tracing::instrument(skip(self))] pub async fn execute(&self, input: Input) -> Result { let schema = self.schema.lock().await; let type_name = input.type_name.as_str(); diff --git a/crates/apollo-mcp-server/src/introspection/tools/search.rs b/crates/apollo-mcp-server/src/introspection/tools/search.rs index 29816bcc..595f11da 100644 --- a/crates/apollo-mcp-server/src/introspection/tools/search.rs +++ b/crates/apollo-mcp-server/src/introspection/tools/search.rs @@ -36,7 +36,7 @@ pub struct Search { } /// Input for the search tool. -#[derive(JsonSchema, Deserialize)] +#[derive(JsonSchema, Deserialize, Debug)] pub struct Input { /// The search terms terms: Vec, @@ -87,6 +87,7 @@ impl Search { }) } + #[tracing::instrument(skip(self))] pub async fn execute(&self, input: Input) -> Result { let mut root_paths = self .index diff --git a/crates/apollo-mcp-server/src/introspection/tools/validate.rs b/crates/apollo-mcp-server/src/introspection/tools/validate.rs index e104cc92..7d403f12 100644 --- a/crates/apollo-mcp-server/src/introspection/tools/validate.rs +++ b/crates/apollo-mcp-server/src/introspection/tools/validate.rs @@ -25,7 +25,7 @@ pub struct Validate { } /// Input for the validate tool -#[derive(JsonSchema, Deserialize)] +#[derive(JsonSchema, Deserialize, Debug)] pub struct Input { /// The GraphQL operation operation: String, @@ -46,6 +46,7 @@ impl Validate { } /// Validates the provided GraphQL query + #[tracing::instrument(skip(self))] pub async fn execute(&self, input: Value) -> Result { let input = serde_json::from_value::(input).map_err(|_| { McpError::new(ErrorCode::INVALID_PARAMS, "Invalid input".to_string(), None) diff --git a/crates/apollo-mcp-server/src/lib.rs b/crates/apollo-mcp-server/src/lib.rs index 5552e193..1737b4e1 100644 --- a/crates/apollo-mcp-server/src/lib.rs +++ b/crates/apollo-mcp-server/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] + pub mod auth; pub mod cors; pub mod custom_scalar_map; @@ -8,7 +10,16 @@ mod graphql; pub mod health; mod introspection; pub mod json_schema; +pub(crate) mod meter; pub mod operations; pub mod sanitize; pub(crate) mod schema_tree_shake; pub mod server; +pub mod telemetry_attributes; + +/// These values are generated at build time by build.rs using telemetry.toml as input. +pub mod generated { + pub mod telemetry { + include!(concat!(env!("OUT_DIR"), "/telemetry_attributes.rs")); + } +} diff --git a/crates/apollo-mcp-server/src/main.rs b/crates/apollo-mcp-server/src/main.rs index 9c6b0341..0d80e937 100644 --- a/crates/apollo-mcp-server/src/main.rs +++ b/crates/apollo-mcp-server/src/main.rs @@ -11,7 +11,6 @@ use clap::Parser; use clap::builder::Styles; use clap::builder::styling::{AnsiColor, Effects}; use runtime::IdOrDefault; -use runtime::logging::Logging; use tracing::{info, warn}; mod runtime; @@ -42,9 +41,7 @@ async fn main() -> anyhow::Result<()> { None => runtime::read_config_from_env().unwrap_or_default(), }; - // WorkerGuard is not used but needed to be at least defined or else the guard - // is cleaned up too early and file appender logging does not work - let _guard = Logging::setup(&config)?; + let _guard = runtime::telemetry::init_tracing_subscriber(&config)?; info!( "Apollo MCP Server v{} // (c) Apollo Graph, Inc. // Licensed under MIT", diff --git a/crates/apollo-mcp-server/src/meter.rs b/crates/apollo-mcp-server/src/meter.rs new file mode 100644 index 00000000..a52f7447 --- /dev/null +++ b/crates/apollo-mcp-server/src/meter.rs @@ -0,0 +1,4 @@ +use opentelemetry::{global, metrics::Meter}; +use std::sync::LazyLock; + +pub static METER: LazyLock = LazyLock::new(|| global::meter(env!("CARGO_PKG_NAME"))); diff --git a/crates/apollo-mcp-server/src/operations/operation.rs b/crates/apollo-mcp-server/src/operations/operation.rs index 6a0ac72e..33a230fc 100644 --- a/crates/apollo-mcp-server/src/operations/operation.rs +++ b/crates/apollo-mcp-server/src/operations/operation.rs @@ -48,6 +48,7 @@ impl Operation { self.inner } + #[tracing::instrument(skip(graphql_schema, custom_scalar_map))] pub fn from_document( raw_operation: RawOperation, graphql_schema: &GraphqlSchema, @@ -138,6 +139,7 @@ impl Operation { } /// Generate a description for an operation based on documentation in the schema + #[tracing::instrument(skip(comments, tree_shaker, graphql_schema))] fn tool_description( comments: Option, tree_shaker: &mut SchemaTreeShaker, @@ -335,6 +337,7 @@ impl graphql::Executable for Operation { } #[allow(clippy::type_complexity)] +#[tracing::instrument(skip_all)] pub fn operation_defs( source_text: &str, allow_mutations: bool, @@ -424,6 +427,7 @@ pub fn operation_name( .to_string()) } +#[tracing::instrument(skip(source_text))] pub fn variable_description_overrides( source_text: &str, operation_definition: &Node, @@ -455,6 +459,7 @@ pub fn variable_description_overrides( argument_overrides_map } +#[tracing::instrument(skip(source_text))] pub fn find_opening_parens_offset( source_text: &str, operation_definition: &Node, @@ -512,6 +517,7 @@ fn tool_character_length(tool: &Tool) -> Result { + tool_schema_string.len()) } +#[tracing::instrument(skip_all)] fn get_json_schema( operation: &Node, schema_argument_descriptions: &HashMap>, diff --git a/crates/apollo-mcp-server/src/operations/operation_source.rs b/crates/apollo-mcp-server/src/operations/operation_source.rs index d34b2a9b..65f49890 100644 --- a/crates/apollo-mcp-server/src/operations/operation_source.rs +++ b/crates/apollo-mcp-server/src/operations/operation_source.rs @@ -22,7 +22,7 @@ use super::RawOperation; const OPERATION_DOCUMENT_EXTENSION: &str = "graphql"; /// The source of the operations exposed as MCP tools -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum OperationSource { /// GraphQL document files Files(Vec), @@ -38,6 +38,7 @@ pub enum OperationSource { } impl OperationSource { + #[tracing::instrument(skip_all, fields(operation_source = ?self))] pub async fn into_stream(self) -> impl Stream { match self { OperationSource::Files(paths) => Self::stream_file_changes(paths).boxed(), @@ -73,6 +74,7 @@ impl OperationSource { } } + #[tracing::instrument] fn stream_file_changes(paths: Vec) -> impl Stream { let path_count = paths.len(); let state = Arc::new(Mutex::new(HashMap::>::new())); diff --git a/crates/apollo-mcp-server/src/runtime.rs b/crates/apollo-mcp-server/src/runtime.rs index 22a2afec..71a39684 100644 --- a/crates/apollo-mcp-server/src/runtime.rs +++ b/crates/apollo-mcp-server/src/runtime.rs @@ -5,6 +5,7 @@ mod config; mod endpoint; +mod filtering_exporter; mod graphos; mod introspection; pub mod logging; @@ -12,6 +13,7 @@ mod operation_source; mod overrides; mod schema_source; mod schemas; +pub mod telemetry; use std::path::Path; @@ -130,8 +132,13 @@ mod test { let config = " endpoint: http://from_file:4000/ "; + let saved_path = std::env::var("PATH").unwrap_or_default(); + let workspace = env!("CARGO_MANIFEST_DIR"); figment::Jail::expect_with(move |jail| { + jail.clear_env(); + jail.set_env("PATH", &saved_path); + jail.set_env("INSTA_WORKSPACE_ROOT", workspace); let path = "config.yaml"; jail.create_file(path, config)?; @@ -269,6 +276,11 @@ mod test { path: None, rotation: Hourly, }, + telemetry: Telemetry { + exporters: None, + service_name: None, + version: None, + }, operations: Infer, overrides: Overrides { disable_type_description: false, diff --git a/crates/apollo-mcp-server/src/runtime/config.rs b/crates/apollo-mcp-server/src/runtime/config.rs index 991a21a5..598462bd 100644 --- a/crates/apollo-mcp-server/src/runtime/config.rs +++ b/crates/apollo-mcp-server/src/runtime/config.rs @@ -8,7 +8,7 @@ use url::Url; use super::{ OperationSource, SchemaSource, endpoint::Endpoint, graphos::GraphOSConfig, - introspection::Introspection, logging::Logging, overrides::Overrides, + introspection::Introspection, logging::Logging, overrides::Overrides, telemetry::Telemetry, }; /// Configuration for the MCP server @@ -43,6 +43,9 @@ pub struct Config { /// Logging configuration pub logging: Logging, + /// Telemetry configuration + pub telemetry: Telemetry, + /// Operations pub operations: OperationSource, diff --git a/crates/apollo-mcp-server/src/runtime/filtering_exporter.rs b/crates/apollo-mcp-server/src/runtime/filtering_exporter.rs new file mode 100644 index 00000000..e56fc3eb --- /dev/null +++ b/crates/apollo-mcp-server/src/runtime/filtering_exporter.rs @@ -0,0 +1,232 @@ +use opentelemetry::{Key, KeyValue}; +use opentelemetry_sdk::Resource; +use opentelemetry_sdk::error::OTelSdkResult; +use opentelemetry_sdk::trace::{SpanData, SpanExporter}; +use std::collections::HashSet; +use std::fmt::Debug; + +#[derive(Debug)] +pub struct FilteringExporter { + inner: E, + omitted: HashSet, +} + +impl FilteringExporter { + pub fn new(inner: E, omitted: impl IntoIterator) -> Self { + Self { + inner, + omitted: omitted.into_iter().collect(), + } + } +} + +impl SpanExporter for FilteringExporter +where + E: SpanExporter + Send + Sync, +{ + fn export(&self, mut batch: Vec) -> impl Future + Send { + for span in &mut batch { + span.attributes + .retain(|kv| filter_omitted_apollo_attributes(kv, &self.omitted)); + } + + self.inner.export(batch) + } + + fn shutdown(&mut self) -> OTelSdkResult { + self.inner.shutdown() + } + fn force_flush(&mut self) -> OTelSdkResult { + self.inner.force_flush() + } + fn set_resource(&mut self, r: &Resource) { + self.inner.set_resource(r) + } +} + +fn filter_omitted_apollo_attributes(kv: &KeyValue, omitted_attributes: &HashSet) -> bool { + !kv.key.as_str().starts_with("apollo.") || !omitted_attributes.contains(&kv.key) +} + +#[cfg(test)] +mod tests { + use crate::runtime::filtering_exporter::FilteringExporter; + use opentelemetry::trace::{SpanContext, SpanKind, Status, TraceState}; + use opentelemetry::{InstrumentationScope, Key, KeyValue, SpanId, TraceFlags, TraceId}; + use opentelemetry_sdk::Resource; + use opentelemetry_sdk::error::{OTelSdkError, OTelSdkResult}; + use opentelemetry_sdk::trace::{SpanData, SpanEvents, SpanExporter, SpanLinks}; + use std::collections::HashSet; + use std::fmt::Debug; + use std::future::ready; + use std::time::SystemTime; + + #[cfg_attr(coverage_nightly, coverage(off))] + fn create_mock_span_data() -> SpanData { + let span_context: SpanContext = SpanContext::new( + TraceId::from_u128(1), + SpanId::from_u64(12345), + TraceFlags::default(), + true, // is_remote + TraceState::default(), + ); + + SpanData { + span_context, + parent_span_id: SpanId::from_u64(54321), + span_kind: SpanKind::Internal, + name: "test-span".into(), + start_time: SystemTime::UNIX_EPOCH, + end_time: SystemTime::UNIX_EPOCH, + attributes: vec![ + KeyValue::new("http.method", "GET"), + KeyValue::new("apollo.mock", "mock"), + ], + dropped_attributes_count: 0, + events: SpanEvents::default(), + links: SpanLinks::default(), + status: Status::Ok, + instrumentation_scope: InstrumentationScope::builder("test-service") + .with_version("1.0.0") + .build(), + } + } + + #[tokio::test] + async fn filtering_exporter_filters_omitted_apollo_attributes() { + #[derive(Debug)] + struct TestExporter {} + + #[cfg_attr(coverage_nightly, coverage(off))] + impl SpanExporter for TestExporter { + fn export(&self, batch: Vec) -> impl Future + Send { + batch.into_iter().for_each(|span| { + if span + .attributes + .iter() + .any(|kv| kv.key.as_str().starts_with("apollo.")) + { + panic!("Omitted attributes were not filtered"); + } + }); + + ready(Ok(())) + } + + fn shutdown(&mut self) -> OTelSdkResult { + Ok(()) + } + + fn force_flush(&mut self) -> OTelSdkResult { + Ok(()) + } + + fn set_resource(&mut self, _resource: &Resource) {} + } + + let mut omitted = HashSet::new(); + omitted.insert(Key::from_static_str("apollo.mock")); + let mock_exporter = TestExporter {}; + let mock_span_data = create_mock_span_data(); + + let filtering_exporter = FilteringExporter::new(mock_exporter, omitted); + filtering_exporter + .export(vec![mock_span_data]) + .await + .expect("Export error"); + } + + #[tokio::test] + async fn filtering_exporter_calls_inner_exporter_on_shutdown() { + #[derive(Debug)] + struct TestExporter {} + + #[cfg_attr(coverage_nightly, coverage(off))] + impl SpanExporter for TestExporter { + fn export(&self, _batch: Vec) -> impl Future + Send { + ready(Err(OTelSdkError::InternalFailure( + "unexpected call".to_string(), + ))) + } + + fn shutdown(&mut self) -> OTelSdkResult { + Ok(()) + } + + fn force_flush(&mut self) -> OTelSdkResult { + Err(OTelSdkError::InternalFailure("unexpected call".to_string())) + } + + fn set_resource(&mut self, _resource: &Resource) { + unreachable!("should not be called"); + } + } + + let mock_exporter = TestExporter {}; + + let mut filtering_exporter = FilteringExporter::new(mock_exporter, HashSet::new()); + assert!(filtering_exporter.shutdown().is_ok()); + } + + #[tokio::test] + async fn filtering_exporter_calls_inner_exporter_on_force_flush() { + #[derive(Debug)] + struct TestExporter {} + + #[cfg_attr(coverage_nightly, coverage(off))] + impl SpanExporter for TestExporter { + fn export(&self, _batch: Vec) -> impl Future + Send { + ready(Err(OTelSdkError::InternalFailure( + "unexpected call".to_string(), + ))) + } + + fn shutdown(&mut self) -> OTelSdkResult { + Err(OTelSdkError::InternalFailure("unexpected call".to_string())) + } + + fn force_flush(&mut self) -> OTelSdkResult { + Ok(()) + } + + fn set_resource(&mut self, _resource: &Resource) { + unreachable!("should not be called"); + } + } + + let mock_exporter = TestExporter {}; + + let mut filtering_exporter = FilteringExporter::new(mock_exporter, HashSet::new()); + assert!(filtering_exporter.force_flush().is_ok()); + } + + #[tokio::test] + async fn filtering_exporter_calls_inner_exporter_on_set_resource() { + #[derive(Debug)] + struct TestExporter {} + + #[cfg_attr(coverage_nightly, coverage(off))] + impl SpanExporter for TestExporter { + fn export(&self, _batch: Vec) -> impl Future + Send { + ready(Err(OTelSdkError::InternalFailure( + "unexpected call".to_string(), + ))) + } + + fn shutdown(&mut self) -> OTelSdkResult { + Err(OTelSdkError::InternalFailure("unexpected call".to_string())) + } + + fn force_flush(&mut self) -> OTelSdkResult { + Err(OTelSdkError::InternalFailure("unexpected call".to_string())) + } + + fn set_resource(&mut self, _resource: &Resource) {} + } + + let mock_exporter = TestExporter {}; + + let mut filtering_exporter = FilteringExporter::new(mock_exporter, HashSet::new()); + filtering_exporter.set_resource(&Resource::builder_empty().build()); + } +} diff --git a/crates/apollo-mcp-server/src/runtime/logging.rs b/crates/apollo-mcp-server/src/runtime/logging.rs index 70a47e30..45c9d256 100644 --- a/crates/apollo-mcp-server/src/runtime/logging.rs +++ b/crates/apollo-mcp-server/src/runtime/logging.rs @@ -12,14 +12,10 @@ use schemars::JsonSchema; use serde::Deserialize; use std::path::PathBuf; use tracing::Level; -use tracing_appender::non_blocking::WorkerGuard; use tracing_appender::rolling::RollingFileAppender; use tracing_subscriber::EnvFilter; +use tracing_subscriber::fmt::Layer; use tracing_subscriber::fmt::writer::BoxMakeWriter; -use tracing_subscriber::layer::SubscriberExt; -use tracing_subscriber::util::SubscriberInitExt; - -use super::Config; /// Logging related options #[derive(Debug, Deserialize, JsonSchema)] @@ -52,31 +48,43 @@ impl Default for Logging { } } +type LoggingLayerResult = ( + Layer< + tracing_subscriber::Registry, + tracing_subscriber::fmt::format::DefaultFields, + tracing_subscriber::fmt::format::Format, + BoxMakeWriter, + >, + Option, +); + impl Logging { - pub fn setup(config: &Config) -> Result, anyhow::Error> { - let mut env_filter = - EnvFilter::from_default_env().add_directive(config.logging.level.into()); + pub fn env_filter(logging: &Logging) -> Result { + let mut env_filter = EnvFilter::from_default_env().add_directive(logging.level.into()); - if config.logging.level == Level::INFO { + if logging.level == Level::INFO { env_filter = env_filter .add_directive("rmcp=warn".parse()?) .add_directive("tantivy=warn".parse()?); } + Ok(env_filter) + } + pub fn logging_layer(logging: &Logging) -> Result { macro_rules! log_error { () => { |e| eprintln!("Failed to setup logging: {e:?}") }; } - let (writer, guard, with_ansi) = match config.logging.path.clone() { + let (writer, guard, with_ansi) = match logging.path.clone() { Some(path) => std::fs::create_dir_all(&path) .map(|_| path) .inspect_err(log_error!()) .ok() .and_then(|path| { RollingFileAppender::builder() - .rotation(config.logging.rotation.clone().into()) + .rotation(logging.rotation.clone().into()) .filename_prefix("apollo_mcp_server") .filename_suffix("log") .build(path) @@ -98,17 +106,13 @@ impl Logging { None => (BoxMakeWriter::new(std::io::stdout), None, true), }; - tracing_subscriber::registry() - .with(env_filter) - .with( - tracing_subscriber::fmt::layer() - .with_writer(writer) - .with_ansi(with_ansi) - .with_target(false), - ) - .init(); - - Ok(guard) + Ok(( + tracing_subscriber::fmt::layer() + .with_writer(writer) + .with_ansi(with_ansi) + .with_target(false), + guard, + )) } } diff --git a/crates/apollo-mcp-server/src/runtime/telemetry.rs b/crates/apollo-mcp-server/src/runtime/telemetry.rs new file mode 100644 index 00000000..d5d0688b --- /dev/null +++ b/crates/apollo-mcp-server/src/runtime/telemetry.rs @@ -0,0 +1,408 @@ +mod sampler; + +use crate::runtime::Config; +use crate::runtime::filtering_exporter::FilteringExporter; +use crate::runtime::logging::Logging; +use crate::runtime::telemetry::sampler::SamplerOption; +use apollo_mcp_server::generated::telemetry::TelemetryAttribute; +use opentelemetry::{Key, KeyValue, global, trace::TracerProvider as _}; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::metrics::{Instrument, Stream}; +use opentelemetry_sdk::{ + Resource, + metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider}, + propagation::TraceContextPropagator, + trace::{RandomIdGenerator, SdkTracerProvider}, +}; +use opentelemetry_semantic_conventions::{ + SCHEMA_URL, + attribute::{DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_VERSION}, +}; +use schemars::JsonSchema; +use serde::Deserialize; +use std::collections::HashSet; +use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +/// Telemetry related options +#[derive(Debug, Deserialize, JsonSchema, Default)] +pub struct Telemetry { + exporters: Option, + service_name: Option, + version: Option, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct Exporters { + metrics: Option, + tracing: Option, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct MetricsExporters { + otlp: Option, + omitted_attributes: Option>, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct OTLPMetricExporter { + endpoint: String, + protocol: String, +} + +impl Default for OTLPMetricExporter { + fn default() -> Self { + Self { + endpoint: "http://localhost:4317".into(), + protocol: "grpc".into(), + } + } +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct TracingExporters { + otlp: Option, + sampler: Option, + omitted_attributes: Option>, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct OTLPTracingExporter { + endpoint: String, + protocol: String, +} + +impl Default for OTLPTracingExporter { + fn default() -> Self { + Self { + endpoint: "http://localhost:4317".into(), + protocol: "grpc".into(), + } + } +} + +fn resource(telemetry: &Telemetry) -> Resource { + let service_name = telemetry + .service_name + .clone() + .unwrap_or_else(|| env!("CARGO_PKG_NAME").to_string()); + + let service_version = telemetry + .version + .clone() + .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); + + let deployment_env = std::env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string()); + + Resource::builder() + .with_service_name(service_name) + .with_schema_url( + [ + KeyValue::new(SERVICE_VERSION, service_version), + KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, deployment_env), + ], + SCHEMA_URL, + ) + .build() +} + +fn init_meter_provider(telemetry: &Telemetry) -> Result { + let metrics_exporters = telemetry + .exporters + .as_ref() + .and_then(|exporters| exporters.metrics.as_ref()); + + let otlp = metrics_exporters + .and_then(|metrics_exporters| metrics_exporters.otlp.as_ref()) + .ok_or_else(|| { + anyhow::anyhow!("No metrics exporters configured, at least one is required") + })?; + + let exporter = match otlp.protocol.as_str() { + "grpc" => opentelemetry_otlp::MetricExporter::builder() + .with_tonic() + .with_endpoint(otlp.endpoint.clone()) + .build()?, + "http/protobuf" => opentelemetry_otlp::MetricExporter::builder() + .with_http() + .with_endpoint(otlp.endpoint.clone()) + .build()?, + other => { + return Err(anyhow::anyhow!( + "Unsupported OTLP protocol: {other}. Supported protocols are: grpc, http/protobuf" + )); + } + }; + + let omitted_attributes: HashSet = metrics_exporters + .and_then(|exporters| exporters.omitted_attributes.clone()) + .unwrap_or_default(); + let included_attributes: Vec = TelemetryAttribute::included_attributes(omitted_attributes) + .iter() + .map(|a| a.to_key()) + .collect(); + + let reader = PeriodicReader::builder(exporter) + .with_interval(std::time::Duration::from_secs(30)) + .build(); + + let filtered_view = move |i: &Instrument| { + if i.name().starts_with("apollo.") { + Stream::builder() + .with_allowed_attribute_keys(included_attributes.clone()) // if available in your version + .build() + .ok() + } else { + None + } + }; + + let meter_provider = MeterProviderBuilder::default() + .with_resource(resource(telemetry)) + .with_reader(reader) + .with_view(filtered_view) + .build(); + + Ok(meter_provider) +} + +fn init_tracer_provider(telemetry: &Telemetry) -> Result { + let tracer_exporters = telemetry + .exporters + .as_ref() + .and_then(|exporters| exporters.tracing.as_ref()); + + let otlp = tracer_exporters + .and_then(|tracing_exporters| tracing_exporters.otlp.as_ref()) + .ok_or_else(|| { + anyhow::anyhow!("No tracing exporters configured, at least one is required") + })?; + + let exporter = match otlp.protocol.as_str() { + "grpc" => opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint(otlp.endpoint.clone()) + .build()?, + "http/protobuf" => opentelemetry_otlp::SpanExporter::builder() + .with_http() + .with_endpoint(otlp.endpoint.clone()) + .build()?, + other => { + return Err(anyhow::anyhow!( + "Unsupported OTLP protocol: {other}. Supported protocols are: grpc, http/protobuf" + )); + } + }; + + let sampler: opentelemetry_sdk::trace::Sampler = tracer_exporters + .as_ref() + .and_then(|e| e.sampler.clone()) + .unwrap_or_default() + .into(); + + let omitted_attributes: HashSet = tracer_exporters + .and_then(|exporters| exporters.omitted_attributes.clone()) + .map(|set| set.iter().map(|a| a.to_key()).collect()) + .unwrap_or_default(); + + let filtering_exporter = FilteringExporter::new(exporter, omitted_attributes); + + let tracer_provider = SdkTracerProvider::builder() + .with_id_generator(RandomIdGenerator::default()) + .with_resource(resource(telemetry)) + .with_batch_exporter(filtering_exporter) + .with_sampler(sampler) + .build(); + + Ok(tracer_provider) +} + +/// Initialize tracing-subscriber and return TelemetryGuard for logging and opentelemetry-related termination processing +pub fn init_tracing_subscriber(config: &Config) -> Result { + let tracer_provider = if let Some(exporters) = &config.telemetry.exporters { + if let Some(_tracing_exporters) = &exporters.tracing { + init_tracer_provider(&config.telemetry)? + } else { + SdkTracerProvider::builder().build() + } + } else { + SdkTracerProvider::builder().build() + }; + let meter_provider = if let Some(exporters) = &config.telemetry.exporters { + if let Some(_metrics_exporters) = &exporters.metrics { + init_meter_provider(&config.telemetry)? + } else { + SdkMeterProvider::builder().build() + } + } else { + SdkMeterProvider::builder().build() + }; + let env_filter = Logging::env_filter(&config.logging)?; + let (logging_layer, logging_guard) = Logging::logging_layer(&config.logging)?; + + let tracer = tracer_provider.tracer("apollo-mcp-trace"); + + global::set_meter_provider(meter_provider.clone()); + global::set_text_map_propagator(TraceContextPropagator::new()); + global::set_tracer_provider(tracer_provider.clone()); + + tracing_subscriber::registry() + .with(logging_layer) + .with(env_filter) + .with(MetricsLayer::new(meter_provider.clone())) + .with(OpenTelemetryLayer::new(tracer)) + .try_init()?; + + Ok(TelemetryGuard { + tracer_provider, + meter_provider, + logging_guard, + }) +} + +pub struct TelemetryGuard { + tracer_provider: SdkTracerProvider, + meter_provider: SdkMeterProvider, + logging_guard: Option, +} + +impl Drop for TelemetryGuard { + fn drop(&mut self) { + if let Err(err) = self.tracer_provider.shutdown() { + tracing::error!("{err:?}"); + } + if let Err(err) = self.meter_provider.shutdown() { + tracing::error!("{err:?}"); + } + drop(self.logging_guard.take()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_config( + service_name: Option<&str>, + version: Option<&str>, + metrics: Option, + tracing: Option, + ) -> Config { + Config { + telemetry: Telemetry { + exporters: Some(Exporters { metrics, tracing }), + service_name: service_name.map(|s| s.to_string()), + version: version.map(|v| v.to_string()), + }, + ..Default::default() + } + } + + #[tokio::test] + async fn guard_is_provided_when_tracing_configured() { + let mut ommitted = HashSet::new(); + ommitted.insert(TelemetryAttribute::RequestId); + + let config = test_config( + Some("test-config"), + Some("1.0.0"), + Some(MetricsExporters { + otlp: Some(OTLPMetricExporter::default()), + omitted_attributes: None, + }), + Some(TracingExporters { + otlp: Some(OTLPTracingExporter::default()), + sampler: Default::default(), + omitted_attributes: Some(ommitted), + }), + ); + // init_tracing_subscriber can only be called once in the test suite to avoid + // panic when calling global::set_tracer_provider multiple times + let guard = init_tracing_subscriber(&config); + assert!(guard.is_ok()); + } + + #[tokio::test] + async fn unknown_protocol_raises_meter_provider_error() { + let config = test_config( + None, + None, + Some(MetricsExporters { + otlp: Some(OTLPMetricExporter { + protocol: "bogus".to_string(), + endpoint: "http://localhost:4317".to_string(), + }), + omitted_attributes: None, + }), + None, + ); + let result = init_meter_provider(&config.telemetry); + assert!( + result + .err() + .map(|e| e.to_string().contains("Unsupported OTLP protocol")) + .unwrap_or(false) + ); + } + + #[tokio::test] + async fn http_protocol_returns_valid_meter_provider() { + let config = test_config( + None, + None, + Some(MetricsExporters { + otlp: Some(OTLPMetricExporter { + protocol: "http/protobuf".to_string(), + endpoint: "http://localhost:4318/v1/metrics".to_string(), + }), + omitted_attributes: None, + }), + None, + ); + let result = init_meter_provider(&config.telemetry); + assert!(result.is_ok()); + } + + #[tokio::test] + async fn unknown_protocol_raises_tracer_provider_error() { + let config = test_config( + None, + None, + None, + Some(TracingExporters { + otlp: Some(OTLPTracingExporter { + protocol: "bogus".to_string(), + endpoint: "http://localhost:4317".to_string(), + }), + sampler: Default::default(), + omitted_attributes: None, + }), + ); + let result = init_tracer_provider(&config.telemetry); + assert!( + result + .err() + .map(|e| e.to_string().contains("Unsupported OTLP protocol")) + .unwrap_or(false) + ); + } + + #[tokio::test] + async fn http_protocol_returns_valid_tracer_provider() { + let config = test_config( + None, + None, + None, + Some(TracingExporters { + otlp: Some(OTLPTracingExporter { + protocol: "http/protobuf".to_string(), + endpoint: "http://localhost:4318/v1/traces".to_string(), + }), + sampler: Default::default(), + omitted_attributes: None, + }), + ); + let result = init_tracer_provider(&config.telemetry); + assert!(result.is_ok()); + } +} diff --git a/crates/apollo-mcp-server/src/runtime/telemetry/sampler.rs b/crates/apollo-mcp-server/src/runtime/telemetry/sampler.rs new file mode 100644 index 00000000..873bccd7 --- /dev/null +++ b/crates/apollo-mcp-server/src/runtime/telemetry/sampler.rs @@ -0,0 +1,98 @@ +use schemars::JsonSchema; +use serde::Deserialize; + +#[derive(Clone, Debug, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields, untagged)] +pub(crate) enum SamplerOption { + /// Sample a given fraction. Fractions >= 1 will always sample. + RatioBased(f64), + Always(Sampler), +} + +#[derive(Clone, Debug, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum Sampler { + /// Always sample + AlwaysOn, + /// Never sample + AlwaysOff, +} + +impl From for opentelemetry_sdk::trace::Sampler { + fn from(s: Sampler) -> Self { + match s { + Sampler::AlwaysOn => opentelemetry_sdk::trace::Sampler::AlwaysOn, + Sampler::AlwaysOff => opentelemetry_sdk::trace::Sampler::AlwaysOff, + } + } +} + +impl From for opentelemetry_sdk::trace::Sampler { + fn from(s: SamplerOption) -> Self { + match s { + SamplerOption::Always(s) => s.into(), + SamplerOption::RatioBased(ratio) => { + opentelemetry_sdk::trace::Sampler::TraceIdRatioBased(ratio) + } + } + } +} + +impl Default for SamplerOption { + fn default() -> Self { + SamplerOption::Always(Sampler::AlwaysOn) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sampler_always_on_maps_to_otel_always_on() { + assert!(matches!( + Sampler::AlwaysOn.into(), + opentelemetry_sdk::trace::Sampler::AlwaysOn + )); + } + + #[test] + fn sampler_always_off_maps_to_otel_always_off() { + assert!(matches!( + Sampler::AlwaysOff.into(), + opentelemetry_sdk::trace::Sampler::AlwaysOff + )); + } + + #[test] + fn sampler_option_always_on_maps_to_otel_always_on() { + assert!(matches!( + SamplerOption::Always(Sampler::AlwaysOn).into(), + opentelemetry_sdk::trace::Sampler::AlwaysOn + )); + } + + #[test] + fn sampler_option_always_off_maps_to_otel_always_off() { + assert!(matches!( + SamplerOption::Always(Sampler::AlwaysOff).into(), + opentelemetry_sdk::trace::Sampler::AlwaysOff + )); + } + + #[test] + fn sampler_option_ratio_based_maps_to_otel_ratio_based_sampler() { + assert!(matches!( + SamplerOption::RatioBased(0.5).into(), + opentelemetry_sdk::trace::Sampler::TraceIdRatioBased(0.5) + )); + } + + #[test] + fn default_sampler_option_is_always_on() { + assert!(matches!( + SamplerOption::default(), + SamplerOption::Always(Sampler::AlwaysOn) + )); + } +} diff --git a/crates/apollo-mcp-server/src/server/states/running.rs b/crates/apollo-mcp-server/src/server/states/running.rs index b1cc4e90..2bdede77 100644 --- a/crates/apollo-mcp-server/src/server/states/running.rs +++ b/crates/apollo-mcp-server/src/server/states/running.rs @@ -3,6 +3,8 @@ use std::sync::Arc; use apollo_compiler::{Schema, validation::Valid}; use headers::HeaderMapExt as _; +use opentelemetry::trace::FutureExt; +use opentelemetry::{Context, KeyValue}; use reqwest::header::HeaderMap; use rmcp::model::Implementation; use rmcp::{ @@ -19,6 +21,8 @@ use tokio_util::sync::CancellationToken; use tracing::{debug, error}; use url::Url; +use crate::generated::telemetry::{TelemetryAttribute, TelemetryMetric}; +use crate::meter; use crate::{ auth::ValidToken, custom_scalar_map::CustomScalarMap, @@ -101,6 +105,7 @@ impl Running { Ok(self) } + #[tracing::instrument(skip_all)] pub(super) async fn update_operations( self, operations: Vec, @@ -142,6 +147,7 @@ impl Running { } /// Notify any peers that tools have changed. Drops unreachable peers from the list. + #[tracing::instrument(skip_all)] async fn notify_tool_list_changed(peers: Arc>>>) { let mut peers = peers.write().await; if !peers.is_empty() { @@ -170,41 +176,51 @@ impl Running { } impl ServerHandler for Running { + #[tracing::instrument(skip(self, _request))] async fn initialize( &self, _request: InitializeRequestParam, context: RequestContext, ) -> Result { + let meter = &meter::METER; + meter + .u64_counter(TelemetryMetric::InitializeCount.as_str()) + .build() + .add(1, &[]); // TODO: how to remove these? let mut peers = self.peers.write().await; peers.push(context.peer); Ok(self.get_info()) } + #[tracing::instrument(skip(self, context, request), fields(apollo.mcp.tool_name = request.name.as_ref(), apollo.mcp.request_id = %context.id.clone()))] async fn call_tool( &self, request: CallToolRequestParam, context: RequestContext, ) -> Result { - let result = match request.name.as_ref() { + let meter = &meter::METER; + let start = std::time::Instant::now(); + let tool_name = request.name.clone(); + let result = match tool_name.as_ref() { INTROSPECT_TOOL_NAME => { self.introspect_tool .as_ref() - .ok_or(tool_not_found(&request.name))? + .ok_or(tool_not_found(&tool_name))? .execute(convert_arguments(request)?) .await } SEARCH_TOOL_NAME => { self.search_tool .as_ref() - .ok_or(tool_not_found(&request.name))? + .ok_or(tool_not_found(&tool_name))? .execute(convert_arguments(request)?) .await } EXPLORER_TOOL_NAME => { self.explorer_tool .as_ref() - .ok_or(tool_not_found(&request.name))? + .ok_or(tool_not_found(&tool_name))? .execute(convert_arguments(request)?) .await } @@ -226,7 +242,7 @@ impl ServerHandler for Running { self.execute_tool .as_ref() - .ok_or(tool_not_found(&request.name))? + .ok_or(tool_not_found(&tool_name))? .execute(graphql::Request { input: Value::from(request.arguments.clone()), endpoint: &self.endpoint, @@ -237,7 +253,7 @@ impl ServerHandler for Running { VALIDATE_TOOL_NAME => { self.validate_tool .as_ref() - .ok_or(tool_not_found(&request.name))? + .ok_or(tool_not_found(&tool_name))? .execute(convert_arguments(request)?) .await } @@ -266,9 +282,10 @@ impl ServerHandler for Running { .lock() .await .iter() - .find(|op| op.as_ref().name == request.name) - .ok_or(tool_not_found(&request.name))? + .find(|op| op.as_ref().name == tool_name) + .ok_or(tool_not_found(&tool_name))? .execute(graphql_request) + .with_context(Context::current()) .await } }; @@ -278,14 +295,37 @@ impl ServerHandler for Running { health_check.record_rejection(); } + let attributes = vec![ + KeyValue::new( + TelemetryAttribute::Success.to_key(), + result.as_ref().is_ok_and(|r| r.is_error != Some(true)), + ), + KeyValue::new(TelemetryAttribute::ToolName.to_key(), tool_name), + ]; + // Record response time and status + meter + .f64_histogram(TelemetryMetric::ToolDuration.as_str()) + .build() + .record(start.elapsed().as_millis() as f64, &attributes); + meter + .u64_counter(TelemetryMetric::ToolCount.as_str()) + .build() + .add(1, &attributes); + result } + #[tracing::instrument(skip_all)] async fn list_tools( &self, _request: Option, _context: RequestContext, ) -> Result { + let meter = &meter::METER; + meter + .u64_counter(TelemetryMetric::ListToolsCount.as_str()) + .build() + .add(1, &[]); Ok(ListToolsResult { next_cursor: None, tools: self @@ -304,6 +344,11 @@ impl ServerHandler for Running { } fn get_info(&self) -> ServerInfo { + let meter = &meter::METER; + meter + .u64_counter(TelemetryMetric::GetInfoCount.as_str()) + .build() + .add(1, &[]); ServerInfo { server_info: Implementation { name: "Apollo MCP Server".to_string(), diff --git a/crates/apollo-mcp-server/src/server/states/starting.rs b/crates/apollo-mcp-server/src/server/states/starting.rs index 88457ac7..c377da5a 100644 --- a/crates/apollo-mcp-server/src/server/states/starting.rs +++ b/crates/apollo-mcp-server/src/server/states/starting.rs @@ -2,6 +2,8 @@ use std::{net::SocketAddr, sync::Arc}; use apollo_compiler::{Name, Schema, ast::OperationType, validation::Valid}; use axum::{Router, extract::Query, http::StatusCode, response::Json, routing::get}; +use axum_otel_metrics::HttpMetricsLayerBuilder; +use axum_tracing_opentelemetry::middleware::{OtelAxumLayer, OtelInResponseLayer}; use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; use rmcp::transport::{StreamableHttpServerConfig, StreamableHttpService}; use rmcp::{ @@ -11,6 +13,7 @@ use rmcp::{ use serde_json::json; use tokio::sync::{Mutex, RwLock}; use tokio_util::sync::CancellationToken; +use tower_http::trace::TraceLayer; use tracing::{Instrument as _, debug, error, info, trace}; use crate::{ @@ -205,6 +208,30 @@ impl Starting { let mut router = with_cors!( with_auth!(axum::Router::new().nest_service("/mcp", service), auth), self.config.cors + ) + .layer(HttpMetricsLayerBuilder::new().build()) + // include trace context as header into the response + .layer(OtelInResponseLayer) + //start OpenTelemetry trace on incoming request + .layer(OtelAxumLayer::default()) + // Add tower-http tracing layer for additional HTTP-level tracing + .layer( + TraceLayer::new_for_http() + .make_span_with(|request: &axum::http::Request<_>| { + tracing::info_span!( + "mcp_server", + method = %request.method(), + uri = %request.uri(), + status_code = tracing::field::Empty, + ) + }) + .on_response( + |response: &axum::http::Response<_>, + _latency: std::time::Duration, + span: &tracing::Span| { + span.record("status", tracing::field::display(response.status())); + }, + ), ); // Add health check endpoint if configured @@ -304,3 +331,53 @@ async fn health_endpoint( Ok((status_code, Json(json!(health)))) } + +#[cfg(test)] +mod tests { + use http::HeaderMap; + use url::Url; + + use crate::health::HealthCheckConfig; + + use super::*; + + #[tokio::test] + async fn start_basic_server() { + let starting = Starting { + config: Config { + transport: Transport::StreamableHttp { + auth: None, + address: "127.0.0.1".parse().unwrap(), + port: 7799, + stateful_mode: false, + }, + endpoint: Url::parse("http://localhost:4000").expect("valid url"), + mutation_mode: MutationMode::All, + execute_introspection: true, + headers: HeaderMap::new(), + validate_introspection: true, + introspect_introspection: true, + search_introspection: true, + introspect_minify: false, + search_minify: false, + explorer_graph_ref: None, + custom_scalar_map: None, + disable_type_description: false, + disable_schema_description: false, + disable_auth_token_passthrough: false, + search_leaf_depth: 5, + index_memory_bytes: 1024 * 1024 * 1024, + health_check: HealthCheckConfig { + enabled: true, + ..Default::default() + }, + cors: Default::default(), + }, + schema: Schema::parse_and_validate("type Query { hello: String }", "test.graphql") + .expect("Valid schema"), + operations: vec![], + }; + let running = starting.start(); + assert!(running.await.is_ok()); + } +} diff --git a/crates/apollo-mcp-server/src/telemetry_attributes.rs b/crates/apollo-mcp-server/src/telemetry_attributes.rs new file mode 100644 index 00000000..49e6ab5f --- /dev/null +++ b/crates/apollo-mcp-server/src/telemetry_attributes.rs @@ -0,0 +1,36 @@ +use crate::generated::telemetry::{ALL_ATTRS, TelemetryAttribute}; +use opentelemetry::Key; +use std::collections::HashSet; + +impl TelemetryAttribute { + pub const fn to_key(self) -> Key { + match self { + TelemetryAttribute::ToolName => { + Key::from_static_str(TelemetryAttribute::ToolName.as_str()) + } + TelemetryAttribute::OperationId => { + Key::from_static_str(TelemetryAttribute::OperationId.as_str()) + } + TelemetryAttribute::OperationSource => { + Key::from_static_str(TelemetryAttribute::OperationSource.as_str()) + } + TelemetryAttribute::Success => { + Key::from_static_str(TelemetryAttribute::Success.as_str()) + } + TelemetryAttribute::RequestId => { + Key::from_static_str(TelemetryAttribute::RequestId.as_str()) + } + TelemetryAttribute::RawOperation => { + Key::from_static_str(TelemetryAttribute::RawOperation.as_str()) + } + } + } + + pub fn included_attributes(omitted: HashSet) -> Vec { + ALL_ATTRS + .iter() + .copied() + .filter(|a| !omitted.contains(a)) + .collect() + } +} diff --git a/crates/apollo-mcp-server/telemetry.toml b/crates/apollo-mcp-server/telemetry.toml new file mode 100644 index 00000000..8135456a --- /dev/null +++ b/crates/apollo-mcp-server/telemetry.toml @@ -0,0 +1,16 @@ +[attributes.apollo.mcp] +tool_name = "The tool name" +operation_id = "The operation id - either persisted query id, operation name, or unknown" +operation_source = "The operation source - either operation (local file/op collection), persisted query, or LLM generated" +request_id = "The request id" +success = "Sucess flag indicator" +raw_operation = "Graphql operation text and metadata used for Tool generation" + +[metrics.apollo.mcp] +"initialize.count" = "Number of times initialize has been called" +"tool.count" = "Number of times call_tool has been called" +"tool.duration" = "Duration of call_tool" +"list_tools.count" = "Number of times list_tools has been called" +"get_info.count" = "Number of times get_info has been called" +"operation.duration" = "Duration of graphql execute" +"operation.count" = "Number of times graphql execute has been called" diff --git a/docs/source/_sidebar.yaml b/docs/source/_sidebar.yaml index 08cc6729..531eaceb 100644 --- a/docs/source/_sidebar.yaml +++ b/docs/source/_sidebar.yaml @@ -32,6 +32,8 @@ items: href: "./cors" - label: "Authorization" href: "./auth" + - label: "Telemetry" + href: "./telemetry" - label: "Best Practices" href: "./best-practices" - label: "Licensing" diff --git a/docs/source/config-file.mdx b/docs/source/config-file.mdx index 9ef0d2f1..54021eaa 100644 --- a/docs/source/config-file.mdx +++ b/docs/source/config-file.mdx @@ -29,6 +29,8 @@ All fields are optional. | `overrides` | `Overrides` | | Overrides for server behavior | | `schema` | `SchemaSource` | | Schema configuration | | `transport` | `Transport` | | The type of server transport to use | +| `telemetry` | `Telemetry` | | Configuration to export metrics and traces via OTLP | + ### GraphOS @@ -224,6 +226,52 @@ transport: - profile ``` +### Telemetry + +| Option | Type | Default | Description | +| :-------------- | :---------- | :-------------------------- | :--------------------------------------- | +| `service_name` | `string` | "apollo-mcp-server" | The service name in telemetry data. | +| `version` | `string` | Current crate version | The service version in telemetry data. | +| `exporters` | `Exporters` | `null` (Telemetry disabled) | Configuration for telemetry exporters. | + +#### Exporters + +| Option | Type | Default | Description | +| :--------- | :---------- | :-------------------------- | :--------------------------------------- | +| `metrics` | `Metrics` | `null` (Metrics disabled) | Configuration for exporting metrics. | +| `tracing` | `Tracing` | `null` (Tracing disabled) | Configuration for exporting traces. | + + +#### Metrics + +| Option | Type | Default | Description | +| :-------------------- | :--------------- | :-------------------------- | :--------------------------------------------- | +| `otlp` | `OTLP Exporter` | `null` (Exporting disabled) | Configuration for exporting metrics via OTLP. | +| `omitted_attributes` | `List` | | List of attributes to be omitted from metrics. | + +#### Traces + +| Option | Type | Default | Description | +| :-------------------- | :--------------- | :-------------------------- | :--------------------------------------------- | +| `otlp` | `OTLP Exporter` | `null` (Exporting disabled) | Configuration for exporting traces via OTLP. | +| `sampler` | `SamplerOption` | `ALWAYS_ON` | Configuration to control sampling of traces. | +| `omitted_attributes` | `List` | | List of attributes to be omitted from traces. | + +#### OTLP Exporter + +| Option | Type | Default | Description | +| :--------- | :-------- | :-------------------------- | :--------------------------------------------------------------- | +| `endpoint` | `URL` | `http://localhost:4137` | URL to export data to. Requires full path. | +| `protocol` | `string` | `grpc` | Protocol for export. `grpc` and `http/protobuf` are supported. | + +#### SamplerOption + +| Option | Type | Description | +| :----------- | :-------- | :------------------------------------------------------- | +| `always_on` | `string` | All traces will be exported. | +| `always_off` | `string` | Sampling is turned off, no traces will be exported. | +| `0.0-1.0` | `f64` | Percentage of traces to export. | + ## Example config file The following example file sets your endpoint to `localhost:4001`, configures transport over Streamable HTTP, enables introspection, and provides two local MCP operations for the server to expose. diff --git a/docs/source/telemetry.mdx b/docs/source/telemetry.mdx new file mode 100644 index 00000000..9b02f395 --- /dev/null +++ b/docs/source/telemetry.mdx @@ -0,0 +1,168 @@ +--- +title: OpenTelemetry Integration +--- + +AI agents create unpredictable usage patterns and complex request flows that are hard to monitor with traditional methods. The Apollo MCP Server's OpenTelemetry integration provides the visibility you need to run a reliable service for AI agents. + +## What you can monitor + +- **Agent behavior**: Which tools and operations are used most frequently +- **Performance**: Response times and bottlenecks across tool executions and GraphQL operations +- **Reliability**: Error rates, failed operations, and request success patterns +- **Distributed request flows**: Complete traces from agent request through your Apollo Router and subgraphs, with automatic trace context propagation + +## How it works + +The server exports metrics, traces, and events using the OpenTelemetry Protocol (OTLP), ensuring compatibility with your existing observability stack and seamless integration with other instrumented Apollo services. + +## Usage guide + +### Quick start: Local development + +The fastest way to see Apollo MCP Server telemetry in action is with a local setup that requires only Docker. + +#### 5-minute setup +1. Start local observability stack: +docker run -p 3000:3000 -p 4317:4317 -p 4318:4318 --rm -ti grafana/otel-lgtm +1. Add telemetry config to your `config.yaml`: + ```yaml + telemetry: + exporters: + metrics: + otlp: + endpoint: "http://localhost:4318/v1/metrics" + protocol: "http/protobuf" + tracing: + otlp: + endpoint: "http://localhost:4318/v1/traces" + protocol: "http/protobuf" + ``` +1. Restart your MCP server with the updated config +1. Open Grafana at `http://localhost:3000` and explore your telemetry data. Default credentials are username `admin` with password `admin`. + +For detailed steps and dashboard examples, see the [complete Grafana setup guide](guides/telemetry-grafana.mdx). + +### Production deployment + +For production environments, configure your MCP server to send telemetry to any OTLP-compatible backend. The Apollo MCP Server uses standard OpenTelemetry protocols, ensuring compatibility with all major observability platforms. + +#### Configuration example + +```yaml +telemetry: + service_name: "mcp-server-prod" # Custom service name + exporters: + metrics: + otlp: + endpoint: "https://your-metrics-endpoint" + protocol: "http/protobuf" # or "grpc" + tracing: + otlp: + endpoint: "https://your-traces-endpoint" + protocol: "http/protobuf" +``` + +#### Observability platform integration + +The MCP server works with any OTLP-compatible backend. Consult your provider's documentation for specific endpoint URLs and authentication: + +- [Datadog OTLP Integration](https://docs.datadoghq.com/opentelemetry/setup/otlp_ingest_in_the_agent/) - Native OTLP support +- [New Relic OpenTelemetry](https://docs.newrelic.com/docs/opentelemetry/best-practices/opentelemetry-otlp/) - Direct OTLP ingestion +- [AWS Observability](https://aws-otel.github.io/docs/introduction) - Via AWS Distro for OpenTelemetry +- [Grafana Cloud](https://grafana.com/docs/grafana-cloud/send-data/otlp/) - Hosted Grafana with OTLP +- [Honeycomb](https://docs.honeycomb.io/getting-data-in/opentelemetry/) - OpenTelemetry-native platform +- [Jaeger](https://www.jaegertracing.io/docs/1.50/deployment/) - Self-hosted tracing +- [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/deployment/) - Self-hosted with flexible routing + +#### Production configuration best practices + +##### Environment and security +```yaml +# Set via environment variable +export ENVIRONMENT=production + +telemetry: + service_name: "apollo-mcp-server" + version: "1.0.0" # Version for correlation + exporters: + metrics: + otlp: + endpoint: "https://secure-endpoint" # Always use HTTPS + protocol: "http/protobuf" # Generally more reliable than gRPC +``` + +##### Performance considerations +- **Protocol choice**: `http/protobuf` is often more reliable through firewalls and load balancers than `grpc` +- **Batch export**: OpenTelemetry automatically batches telemetry data for efficiency +- **Network timeouts**: Default timeouts are usually appropriate, but monitor for network issues + +##### Resource correlation +- The `ENVIRONMENT` variable automatically tags all telemetry with `deployment.environment.name` +- Use consistent `service_name` across all your Apollo infrastructure (Router, subgraphs, MCP server) +- Set `version` to track releases and correlate issues with deployments + +#### Troubleshooting + +##### Common issues +- **Connection refused**: Verify endpoint URL and network connectivity +- **Authentication errors**: Check if your provider requires API keys or special headers +- **Missing data**: Confirm your observability platform supports OTLP and is configured to receive data +- **High memory usage**: Monitor telemetry export frequency and consider sampling for high-volume environments + +##### Verification +```bash +# Check if telemetry is being exported (look for connection attempts) +curl -v https://your-endpoint/v1/metrics + +# Monitor server logs for OpenTelemetry export errors +./apollo-mcp-server --config config.yaml 2>&1 | grep -i "otel\|telemetry" +``` + +## Configuration Reference + +The OpenTelemetry integration is configured via the `telemetry` section of the [configuration reference page](/apollo-mcp-server/config-file#telemetry). + +## Emitted Metrics + +The server emits the following metrics, which are invaluable for monitoring and alerting. All duration metrics are in milliseconds. + +| Metric Name | Type | Description | Attributes | +|---|---|---|---| +| `apollo.mcp.initialize.count` | Counter | Incremented for each `initialize` request. | (none) | +| `apollo.mcp.list_tools.count` | Counter | Incremented for each `list_tools` request. | (none) | +| `apollo.mcp.get_info.count` | Counter | Incremented for each `get_info` request. | (none) | +| `apollo.mcp.tool.count` | Counter | Incremented for each tool call. | `tool_name`, `success` (bool) | +| `apollo.mcp.tool.duration` | Histogram | Measures the execution duration of each tool call. | `tool_name`, `success` (bool) | +| `apollo.mcp.operation.count`| Counter | Incremented for each downstream GraphQL operation executed by a tool. | `operation.id`, `operation.type` ("persisted_query" or "operation"), `success` (bool) | +| `apollo.mcp.operation.duration`| Histogram | Measures the round-trip duration of each downstream GraphQL operation. | `operation.id`, `operation.type`, `success` (bool) | + +In addition to these metrics, the server also emits standard [HTTP server metrics](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/) (e.g., `http.server.duration`, `http.server.active_requests`) courtesy of the `axum-otel-metrics` library. + + +## Emitted Traces + +Spans are generated for the following actions: + +- **Incoming HTTP Requests**: A root span is created for every HTTP request to the MCP server. +- **MCP Handler Methods**: Nested spans are created for each of the main MCP protocol methods (`initialize`, `call_tool`, `list_tools`). +- **Tool Execution**: `call_tool` spans contain nested spans for the specific tool being executed (e.g., `introspect`, `search`, or a custom GraphQL operation). +- **Downstream GraphQL Calls**: The `execute` tool and custom operation tools create child spans for their outgoing `reqwest` HTTP calls, capturing the duration of the downstream request. The `traceparent` and `tracestate` headers are propagated automatically, enabling distributed traces. + +### Cardinality Control + +High-cardinality metrics can occur in MCP Servers with large number of tools or when clients are allowed to generate freeform operations. +To prevent performance issues and reduce costs, the Apollo MCP Server provides two mechanisms to control metric cardinality, trace sampling and attribute filtering. + +#### Trace Sampling + +Configure the Apollo MCP Server to sample traces sent to your OpenTelemetry Collector using the `sampler` field in the `telemetry.tracing` configuration: + +- **always_on** - Send every trace +- **always_off** - Disable trace collection entirely +- **0.0-1.0** - Send a specified percentage of traces + +#### Attribute Filtering + +The Apollo MCP Server configuration also allows for omitting attributes such as `tool_name` or `operation_id` that can often lead to high cardinality metrics in systems that treat each collected attribute value as a new metric. +Both traces and metrics have an `omitted_attributes` option that takes a list of strings. Any attribute name in the list will be filtered out and not sent to the collector. +For detailed configuration options, see the [telemetry configuration reference](/apollo-mcp-server/config-file#telemetry).