From 98818c706dce832a656a7181eb9e32a757a176b5 Mon Sep 17 00:00:00 2001 From: Luc Varoqui Date: Wed, 14 Oct 2026 12:09:26 +0200 Subject: [PATCH 1/4] feat(divan): export benchmark tracing spans over OTLP behind an otel feature Add an opt-in `otel` feature to `codspeed` and `codspeed-divan-compat`. When `CODSPEED_OTEL_ENDPOINT` is set, `divan::main()` installs a global `tracing` subscriber that exports the spans and events emitted by benchmarks over OTLP/HTTP, filtered by `CODSPEED_OTEL_FILTER` (defaults to `info`), and flushes them when the run ends. Spans from the exporter's own HTTP stack are always filtered out to avoid an export loop. Benchmarks that already set a global subscriber are left untouched; they can compose `divan::otel::layer()` into their own subscriber instead. Closes COD-3862 Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 969 +++++++++++++++++--- crates/codspeed/Cargo.toml | 25 + crates/codspeed/src/lib.rs | 2 + crates/codspeed/src/otel.rs | 136 +++ crates/divan_compat/Cargo.toml | 12 + crates/divan_compat/README.md | 36 + crates/divan_compat/benches/otel_example.rs | 32 + crates/divan_compat/src/lib.rs | 12 + 8 files changed, 1119 insertions(+), 105 deletions(-) create mode 100644 crates/codspeed/src/otel.rs create mode 100644 crates/divan_compat/benches/otel_example.rs diff --git a/Cargo.lock b/Cargo.lock index 51960382..34c1ae2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,21 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "addr2line" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "aho-corasick" version = "1.1.3" @@ -201,7 +186,7 @@ dependencies = [ "polling 2.8.0", "rustix 0.37.27", "slab", - "socket2", + "socket2 0.4.10", "waker-fn", ] @@ -353,6 +338,17 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -366,19 +362,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] -name = "backtrace" -version = "0.3.73" +name = "base64" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "bencher" @@ -394,9 +381,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "blocking" @@ -434,6 +421,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "camino" version = "1.2.1" @@ -572,7 +565,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -592,10 +585,16 @@ dependencies = [ "glob", "libc", "nix", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "serde", "serde_json", "statrs", "tempfile", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", ] [[package]] @@ -642,7 +641,7 @@ dependencies = [ "oorandom", "plotters", "quickcheck", - "rand", + "rand 0.8.5", "rayon", "regex", "serde", @@ -664,6 +663,7 @@ dependencies = [ "codspeed-divan-compat-macros", "codspeed-divan-compat-walltime", "regex", + "tracing", ] [[package]] @@ -685,7 +685,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -714,7 +714,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -800,6 +800,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "divan-macros" version = "0.1.17" @@ -808,7 +819,7 @@ checksum = "8dc51d98e636f5e3b0759a39257458b22619cac7e96d932da6eeb052891bb67c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -831,12 +842,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -907,6 +918,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -989,6 +1009,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -1010,6 +1041,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1031,20 +1063,26 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", + "wasip2", ] [[package]] -name = "gimli" -version = "0.29.0" +name = "getrandom" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] [[package]] name = "glob" @@ -1098,6 +1136,198 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.5", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1128,6 +1358,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + [[package]] name = "is-terminal" version = "0.4.12" @@ -1171,10 +1407,12 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -1187,6 +1425,12 @@ dependencies = [ "log", ] +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + [[package]] name = "libc" version = "0.2.182" @@ -1215,15 +1459,30 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + [[package]] name = "log" -version = "0.4.22" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" dependencies = [ "value-bag", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1240,12 +1499,14 @@ dependencies = [ ] [[package]] -name = "miniz_oxide" -version = "0.7.4" +name = "mio" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ - "adler", + "libc", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -1254,7 +1515,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -1294,15 +1555,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "object" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.19.0" @@ -1315,12 +1567,88 @@ version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +[[package]] +name = "opentelemetry" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f1a6ac9c875a4941e4c7c085b61e8f4daa326de03b4701768c3491a6ad9ee" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror", +] + +[[package]] +name = "opentelemetry-http" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302249bd8ee804789faaa7b5b2cbf79bc61cb864714079c644e3828af5b7e811" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0756af24841e367128ef31c831999937b8b5c5197d47914590d8f4e2e88c2e1a" +dependencies = [ + "http", + "httpdate", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "thiserror", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d576f5dddc3299341bcd85a04be17b2564844c0c9563214f02d85f2524d1afae" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372a912db2974d1990207006fd0e3aa0ef3fd4ad3be43c65cfaadf2206866061" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "portable-atomic", + "rand 0.9.5", + "thiserror", +] + [[package]] name = "parking" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1403,6 +1731,21 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -1460,24 +1803,53 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "quickcheck" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "rand", + "rand 0.8.5", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1491,8 +1863,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1502,7 +1884,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1514,6 +1906,15 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rayon" version = "1.10.0" @@ -1579,10 +1980,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] -name = "rustc-demangle" -version = "0.1.24" +name = "reqwest" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] [[package]] name = "rustix" @@ -1604,13 +2030,19 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + [[package]] name = "ryu" version = "1.0.18" @@ -1663,7 +2095,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -1688,6 +2120,15 @@ dependencies = [ "serde", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "2.0.1" @@ -1712,6 +2153,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + [[package]] name = "smol" version = "1.3.0" @@ -1756,6 +2203,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "statrs" version = "0.18.0" @@ -1783,6 +2246,37 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "tempfile" version = "3.7.1" @@ -1838,7 +2332,26 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", ] [[package]] @@ -1853,12 +2366,15 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.1" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "backtrace", + "libc", + "mio", "pin-project-lite", + "socket2 0.6.5", + "windows-sys 0.61.2", ] [[package]] @@ -1925,21 +2441,117 @@ dependencies = [ "winnow 1.0.3", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a904802a1b902f43638b677ff2a650847e3b4404101b6c586d648e8c1e3e8fe" +dependencies = [ + "js-sys", + "opentelemetry", + "tracing", + "tracing-core", + "tracing-subscriber", + "web-time", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "once_cell", + "regex-automata", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicode-ident" @@ -1947,17 +2559,41 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "value-bag" -version = "1.9.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" +checksum = "2799ffb329a792ecfd902b71306c8a815a6ef1c0470fa9953a6aa4d4cecbe511" [[package]] name = "wait-timeout" @@ -1984,6 +2620,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1991,27 +2636,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.92" +name = "wasip2" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "cfg-if", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" +name = "wasm-bindgen" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ - "bumpalo", - "log", + "cfg-if", "once_cell", - "proc-macro2", - "quote", - "syn", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -2029,9 +2671,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2039,22 +2681,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", - "wasm-bindgen-backend", + "syn 3.0.9", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" @@ -2066,6 +2711,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2097,6 +2752,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.48.0" @@ -2115,6 +2776,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2254,6 +2924,41 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -2272,5 +2977,59 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", ] diff --git a/crates/codspeed/Cargo.toml b/crates/codspeed/Cargo.toml index ffec9b8e..fad13b03 100644 --- a/crates/codspeed/Cargo.toml +++ b/crates/codspeed/Cargo.toml @@ -28,6 +28,31 @@ serde = { workspace = true } serde_json = { workspace = true } statrs = { version = "0.18.0", default-features = false } +opentelemetry = { version = "0.33", default-features = false, features = ["trace"], optional = true } +opentelemetry_sdk = { version = "0.33", default-features = false, features = ["trace"], optional = true } +opentelemetry-otlp = { version = "0.33", default-features = false, features = [ + "trace", + "http-proto", + "reqwest-blocking-client", +], optional = true } +tracing = { version = "0.1", default-features = false, features = ["std"], optional = true } +tracing-opentelemetry = { version = "0.34", default-features = false, optional = true } +tracing-subscriber = { version = "0.3", default-features = false, features = [ + "std", + "registry", + "env-filter", +], optional = true } + +[features] +otel = [ + "dep:opentelemetry", + "dep:opentelemetry_sdk", + "dep:opentelemetry-otlp", + "dep:tracing", + "dep:tracing-opentelemetry", + "dep:tracing-subscriber", +] + [[bench]] name = "native" harness = false diff --git a/crates/codspeed/src/lib.rs b/crates/codspeed/src/lib.rs index 929c3662..cf7aa7fa 100644 --- a/crates/codspeed/src/lib.rs +++ b/crates/codspeed/src/lib.rs @@ -4,6 +4,8 @@ pub mod instrument_hooks; mod macros; mod measurement; +#[cfg(feature = "otel")] +pub mod otel; mod request; pub mod utils; pub mod walltime_results; diff --git a/crates/codspeed/src/otel.rs b/crates/codspeed/src/otel.rs new file mode 100644 index 00000000..f2c7f306 --- /dev/null +++ b/crates/codspeed/src/otel.rs @@ -0,0 +1,136 @@ +//! Export of the `tracing` spans and events emitted by benchmarks over OTLP/HTTP. +//! +//! Everything is a no-op unless [`ENDPOINT_ENV`] is set. + +use opentelemetry::trace::TracerProvider as _; +use opentelemetry_otlp::{SpanExporter, WithExportConfig}; +use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; +use tracing::Subscriber; +use tracing_subscriber::{layer::SubscriberExt, registry::LookupSpan, EnvFilter, Layer}; + +/// Base URL of the OTLP/HTTP receiver, e.g. `http://localhost:4318`. +pub const ENDPOINT_ENV: &str = "CODSPEED_OTEL_ENDPOINT"; + +/// `EnvFilter` directives selecting the exported spans and events. Defaults to `info`. +pub const FILTER_ENV: &str = "CODSPEED_OTEL_FILTER"; + +const DEFAULT_FILTER: &str = "info"; + +/// Spans emitted by the exporter itself would be exported in turn, in an endless loop. +const EXPORTER_FILTER_DIRECTIVES: &[&str] = &[ + "h2=off", + "hyper=off", + "hyper_util=off", + "opentelemetry=off", + "opentelemetry_otlp=off", + "opentelemetry_sdk=off", + "opentelemetry_http=off", + "reqwest=off", + "tower=off", +]; + +/// Flushes and shuts down the exporter when dropped. +#[must_use = "dropping the guard shuts the exporter down"] +pub struct OtelGuard { + provider: SdkTracerProvider, +} + +impl Drop for OtelGuard { + fn drop(&mut self) { + if let Err(error) = self.provider.shutdown() { + eprintln!("Warning: failed to flush OpenTelemetry spans: {error}"); + } + } +} + +/// Builds a layer exporting spans and events to the receiver at +/// [`ENDPOINT_ENV`], to add to a subscriber of your own. Returns `None` when +/// the variable is unset. +/// +/// Keep the returned guard alive until the benchmarks are done. +pub fn layer() -> Option<(impl Layer, OtelGuard)> +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + let provider = build_provider()?; + let layer = tracing_opentelemetry::layer() + .with_tracer(provider.tracer("codspeed")) + .with_filter(build_filter()); + Some((layer, OtelGuard { provider })) +} + +/// Installs a global `tracing` subscriber exporting spans and events to the receiver at +/// [`ENDPOINT_ENV`], and registers its provider as the global OpenTelemetry tracer provider. +/// +/// Returns `None` when the variable is unset or a global subscriber is already set. +pub fn init() -> Option { + if tracing::dispatcher::has_been_set() { + return None; + } + + let (layer, guard) = layer()?; + let subscriber = tracing_subscriber::registry().with(layer); + if tracing::subscriber::set_global_default(subscriber).is_err() { + return None; + } + opentelemetry::global::set_tracer_provider(guard.provider.clone()); + Some(guard) +} + +fn build_provider() -> Option { + let endpoint = std::env::var(ENDPOINT_ENV) + .ok() + .filter(|endpoint| !endpoint.is_empty())?; + let traces_endpoint = format!("{}/v1/traces", endpoint.trim_end_matches('/')); + + let exporter = match SpanExporter::builder() + .with_http() + .with_endpoint(traces_endpoint) + .build() + { + Ok(exporter) => exporter, + Err(error) => { + eprintln!("Warning: failed to build the OpenTelemetry exporter: {error}"); + return None; + } + }; + + Some( + SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .with_resource( + Resource::builder() + .with_service_name(service_name()) + .build(), + ) + .build(), + ) +} + +fn build_filter() -> EnvFilter { + let user_directives = std::env::var(FILTER_ENV).unwrap_or_else(|_| DEFAULT_FILTER.to_owned()); + let directives = std::iter::once(user_directives.as_str()) + .chain(EXPORTER_FILTER_DIRECTIVES.iter().copied()) + .collect::>() + .join(","); + EnvFilter::builder().parse_lossy(directives) +} + +/// The benchmark executable name, without the hash cargo appends to it. +fn service_name() -> String { + let file_stem = std::env::current_exe().ok().and_then(|exe| { + exe.file_stem() + .map(|stem| stem.to_string_lossy().into_owned()) + }); + match file_stem { + Some(stem) => match stem.rsplit_once('-') { + Some((name, hash)) if is_cargo_hash(hash) => name.to_owned(), + _ => stem, + }, + None => "codspeed-benchmark".to_owned(), + } +} + +fn is_cargo_hash(s: &str) -> bool { + s.len() == 16 && s.chars().all(|c| c.is_ascii_hexdigit()) +} diff --git a/crates/divan_compat/Cargo.toml b/crates/divan_compat/Cargo.toml index 8b015d31..2cbab7ff 100644 --- a/crates/divan_compat/Cargo.toml +++ b/crates/divan_compat/Cargo.toml @@ -24,6 +24,13 @@ codspeed-divan-compat-macros = { version = "=5.0.2", path = './macros' } regex = "1.11.3" clap = { version = "4", default-features = false, features = ["std", "env"] } +[features] +# Export the `tracing` spans and events of benchmarks over OTLP to `CODSPEED_OTEL_ENDPOINT` +otel = ["codspeed/otel"] + +[dev-dependencies] +tracing = "0.1" + [[bench]] name = "basic_example" harness = false @@ -43,3 +50,8 @@ harness = false [[bench]] name = "fib_example" harness = false + +[[bench]] +name = "otel_example" +harness = false +required-features = ["otel"] diff --git a/crates/divan_compat/README.md b/crates/divan_compat/README.md index dc448a89..cb2e4404 100644 --- a/crates/divan_compat/README.md +++ b/crates/divan_compat/README.md @@ -75,6 +75,42 @@ Done running my_benchmark Finished running 1 benchmark suite(s) ``` +### Exporting `tracing` spans with OpenTelemetry + +With the opt-in `otel` feature, `divan::main()` installs a global `tracing` subscriber that exports +the spans emitted by your benchmarks, and the events recorded inside them, over OTLP/HTTP: + +```toml title="Cargo.toml" +[dev-dependencies] +divan = { package = "codspeed-divan-compat", version = "*", features = ["otel"] } +``` + +- `CODSPEED_OTEL_ENDPOINT`: base URL of the OTLP/HTTP receiver, e.g. `http://localhost:4318`. + Nothing is exported when it is unset. +- `CODSPEED_OTEL_FILTER`: [`EnvFilter`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) + directives selecting what is exported. Defaults to `info`. + +The provider is also registered as the global OpenTelemetry tracer provider, and spans are flushed +when `divan::main()` returns. + +If your benchmarks already set a global subscriber before calling `divan::main()`, nothing is +installed. Add the CodSpeed layer to your subscriber instead, and keep its guard alive until the +benchmarks are done: + +```rust +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +fn main() { + let (otel_layer, _otel_guard) = divan::otel::layer().unzip(); + tracing_subscriber::registry() + .with(otel_layer) + .with(tracing_subscriber::fmt::layer()) + .init(); + + divan::main(); +} +``` + ### Not supported: - [`divan::bench(crate = xxx)`](https://docs.rs/divan/latest/divan/attr.bench.html#crate): due to how the compatibility layer works internally, we do not plan to support this feature. diff --git a/crates/divan_compat/benches/otel_example.rs b/crates/divan_compat/benches/otel_example.rs new file mode 100644 index 00000000..57375d9a --- /dev/null +++ b/crates/divan_compat/benches/otel_example.rs @@ -0,0 +1,32 @@ +use tracing::{debug, debug_span, info, info_span, warn}; + +fn parse(input: &str) -> Vec { + let _span = info_span!("parse", input_len = input.len()).entered(); + let values: Vec = input + .split(',') + .filter_map(|field| field.parse().ok()) + .collect(); + info!(count = values.len(), "parsed values"); + values +} + +fn sum(values: &[u32]) -> u32 { + let _span = info_span!("sum").entered(); + let _filtered_out = debug_span!("sum_inner").entered(); + debug!("filtered out by the default filter"); + values.iter().sum() +} + +#[codspeed_divan_compat::bench] +fn parse_and_sum() -> u32 { + let _span = info_span!("parse_and_sum").entered(); + let values = parse("1,2,x,3"); + if values.len() < 4 { + warn!(skipped = 4 - values.len(), "skipped invalid fields"); + } + sum(&values) +} + +fn main() { + codspeed_divan_compat::main(); +} diff --git a/crates/divan_compat/src/lib.rs b/crates/divan_compat/src/lib.rs index b977abfa..ec2a56d6 100644 --- a/crates/divan_compat/src/lib.rs +++ b/crates/divan_compat/src/lib.rs @@ -1,4 +1,6 @@ pub use codspeed::codspeed_uri; +#[cfg(feature = "otel")] +pub use codspeed::otel; #[cfg(not(codspeed))] mod compat_divan { @@ -19,3 +21,13 @@ mod compat_divan { } pub use compat_divan::*; + +/// Runs all registered benchmarks. +/// +/// With the `otel` feature, spans and events are exported for the duration of the run. +pub fn main() { + #[cfg(feature = "otel")] + let _otel_guard = otel::init(); + + compat_divan::main(); +} From 6355f27f48a60f6557baf867ca771288b9c56e1a Mon Sep 17 00:00:00 2001 From: Luc Varoqui Date: Wed, 14 Oct 2026 12:32:15 +0200 Subject: [PATCH 2/4] fix(codspeed): keep the otel dependencies within MSRV and minimal versions `reqwest` pulls `idna_adapter` 1.2.2 and the `icu_*` 2.x crates, which require rustc 1.88. Resolve them with the MSRV-aware fallback resolver so the lockfile builds on rustc 1.85. `sharded-slab` declares `lazy_static = "1"` but fails to build with 1.0.x, which breaks the minimal-versions check. Raise the floor with an optional direct `lazy_static = "1.1"` dependency enabled by the `otel` feature. Refs COD-3862 Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 162 +++++++++++++++++++++++-------------- crates/codspeed/Cargo.toml | 3 + 2 files changed, 104 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34c1ae2e..38f56e02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -583,6 +583,7 @@ dependencies = [ "colored", "getrandom 0.4.3", "glob", + "lazy_static", "libc", "nix", "opentelemetry", @@ -1226,23 +1227,21 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", - "potential_utf", - "utf8_iter", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locale_core" -version = "2.3.0" +name = "icu_locid" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", @@ -1251,62 +1250,99 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + [[package]] name = "icu_normalizer" -version = "2.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ + "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "utf8_iter", + "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "2.3.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" -version = "2.3.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", - "icu_locale_core", + "icu_locid_transform", "icu_properties_data", "icu_provider", - "zerotrie", + "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.3.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" -version = "2.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", - "icu_locale_core", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", "writeable", "yoke", "zerofrom", - "zerotrie", "zerovec", ] +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "idna" version = "1.1.0" @@ -1320,9 +1356,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", @@ -1461,9 +1497,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "litemap" -version = "0.8.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "log" @@ -1737,15 +1773,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" -[[package]] -name = "potential_utf" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" -dependencies = [ - "zerovec", -] - [[package]] name = "ppv-lite86" version = "0.2.20" @@ -2266,6 +2293,17 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "synstructure" version = "0.14.0" @@ -2346,9 +2384,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", @@ -2571,6 +2609,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2930,18 +2974,25 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + [[package]] name = "writeable" -version = "0.6.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "yoke" -version = "0.8.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ + "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -2949,14 +3000,14 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", - "synstructure", + "syn 2.0.96", + "synstructure 0.13.2", ] [[package]] @@ -2998,25 +3049,14 @@ dependencies = [ "proc-macro2", "quote", "syn 3.0.9", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", + "synstructure 0.14.0", ] [[package]] name = "zerovec" -version = "0.11.8" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", @@ -3025,11 +3065,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.6" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +checksum = "3e3c6377872d72510393f688a555d7097b0f741995c7a00f0407f786dd486b2d" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.96", ] diff --git a/crates/codspeed/Cargo.toml b/crates/codspeed/Cargo.toml index fad13b03..460b87e0 100644 --- a/crates/codspeed/Cargo.toml +++ b/crates/codspeed/Cargo.toml @@ -42,6 +42,8 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [ "registry", "env-filter", ], optional = true } +# Unused directly: raises the floor of `sharded-slab`'s `lazy_static = "1"`, which fails to build below 1.1 +lazy_static = { version = "1.1", optional = true } [features] otel = [ @@ -51,6 +53,7 @@ otel = [ "dep:tracing", "dep:tracing-opentelemetry", "dep:tracing-subscriber", + "dep:lazy_static", ] [[bench]] From e359614f80dea813c09236d4ef906cdb40757e40 Mon Sep 17 00:00:00 2001 From: Luc Varoqui Date: Wed, 14 Oct 2026 18:19:23 +0200 Subject: [PATCH 3/4] feat(codspeed): label exported spans with the running benchmark URI Spans exported with the otel feature carried nothing tying them to a benchmark, so a suite with several benchmarks could not be told apart. Add `benchmark_scope::enter`/`exit`, which the integrations call around each benchmark, outside the measured section. A span processor sets the `codspeed.benchmark.uri` attribute on every span started inside the scope. The scope is process-wide, so spans of threaded benchmarks are labelled too. Without the otel feature, both calls do nothing. The divan walltime harness now computes the benchmark URI before running it instead of after. Refs COD-3862 Co-Authored-By: Claude Opus 5.5 --- crates/codspeed/src/benchmark_scope.rs | 21 ++++ crates/codspeed/src/codspeed.rs | 11 +- crates/codspeed/src/lib.rs | 1 + crates/codspeed/src/otel.rs | 110 ++++++++++++++++++- crates/divan_compat/divan_fork/src/divan.rs | 114 ++++++++++---------- 5 files changed, 198 insertions(+), 59 deletions(-) create mode 100644 crates/codspeed/src/benchmark_scope.rs diff --git a/crates/codspeed/src/benchmark_scope.rs b/crates/codspeed/src/benchmark_scope.rs new file mode 100644 index 00000000..93fdd15f --- /dev/null +++ b/crates/codspeed/src/benchmark_scope.rs @@ -0,0 +1,21 @@ +//! The benchmark being run, which the exported telemetry is attributed to. +//! +//! Without the `otel` feature, entering and exiting a scope does nothing. + +/// Marks the start of the benchmark `uri`. Spans started until [`exit`] carry its URI. +/// +/// Call it outside the measured section. +pub fn enter(uri: &str) { + #[cfg(feature = "otel")] + crate::otel::enter_benchmark(uri); + #[cfg(not(feature = "otel"))] + let _ = uri; +} + +/// Marks the end of the current benchmark. +/// +/// Call it outside the measured section. +pub fn exit() { + #[cfg(feature = "otel")] + crate::otel::exit_benchmark(); +} diff --git a/crates/codspeed/src/codspeed.rs b/crates/codspeed/src/codspeed.rs index 1a4644d2..02d91a04 100644 --- a/crates/codspeed/src/codspeed.rs +++ b/crates/codspeed/src/codspeed.rs @@ -81,6 +81,7 @@ impl CodSpeed { #[inline(always)] pub fn start_benchmark(&mut self, name: &str) { self.current_benchmark = CString::new(name).expect("CString::new failed"); + crate::benchmark_scope::enter(name); if let InstrumentationStatus::InstrumentHooks(hooks_instance) = &self.instrumentation_status { @@ -99,9 +100,15 @@ impl CodSpeed { // The overhead in case we are not running valgrind is extremely low, and we do not want to // add a conditionnal branch to valgrind measurements. measurement::stop(&self.current_benchmark); - if let InstrumentationStatus::InstrumentHooks(hooks_instance) = &self.instrumentation_status - { + let hooks_instance = match &self.instrumentation_status { + InstrumentationStatus::InstrumentHooks(hooks_instance) => Some(*hooks_instance), + _ => None, + }; + if let Some(hooks_instance) = hooks_instance { let _ = hooks_instance.stop_benchmark(); + } + crate::benchmark_scope::exit(); + if let Some(hooks_instance) = hooks_instance { let _ = hooks_instance.set_executed_benchmark(&self.current_benchmark.to_string_lossy()); } diff --git a/crates/codspeed/src/lib.rs b/crates/codspeed/src/lib.rs index cf7aa7fa..d0025727 100644 --- a/crates/codspeed/src/lib.rs +++ b/crates/codspeed/src/lib.rs @@ -1,3 +1,4 @@ +pub mod benchmark_scope; pub mod codspeed; pub mod instrument_hooks; diff --git a/crates/codspeed/src/otel.rs b/crates/codspeed/src/otel.rs index f2c7f306..52ad9270 100644 --- a/crates/codspeed/src/otel.rs +++ b/crates/codspeed/src/otel.rs @@ -2,9 +2,15 @@ //! //! Everything is a no-op unless [`ENDPOINT_ENV`] is set. -use opentelemetry::trace::TracerProvider as _; +use std::sync::{Arc, PoisonError, RwLock}; +use std::time::Duration; + +use opentelemetry::trace::{Span as _, TracerProvider as _}; +use opentelemetry::{Context, KeyValue}; use opentelemetry_otlp::{SpanExporter, WithExportConfig}; -use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; +use opentelemetry_sdk::error::OTelSdkResult; +use opentelemetry_sdk::trace::{SdkTracerProvider, Span, SpanData, SpanProcessor}; +use opentelemetry_sdk::Resource; use tracing::Subscriber; use tracing_subscriber::{layer::SubscriberExt, registry::LookupSpan, EnvFilter, Layer}; @@ -16,6 +22,11 @@ pub const FILTER_ENV: &str = "CODSPEED_OTEL_FILTER"; const DEFAULT_FILTER: &str = "info"; +/// Span attribute holding the URI of the benchmark running when the span started. +pub const BENCHMARK_URI_ATTRIBUTE: &str = "codspeed.benchmark.uri"; + +static CURRENT_BENCHMARK: RwLock>> = RwLock::new(None); + /// Spans emitted by the exporter itself would be exported in turn, in an endless loop. const EXPORTER_FILTER_DIRECTIVES: &[&str] = &[ "h2=off", @@ -97,6 +108,7 @@ fn build_provider() -> Option { Some( SdkTracerProvider::builder() + .with_span_processor(BenchmarkLabelProcessor) .with_batch_exporter(exporter) .with_resource( Resource::builder() @@ -107,6 +119,44 @@ fn build_provider() -> Option { ) } +pub(crate) fn enter_benchmark(uri: &str) { + *CURRENT_BENCHMARK + .write() + .unwrap_or_else(PoisonError::into_inner) = Some(Arc::from(uri)); +} + +pub(crate) fn exit_benchmark() { + *CURRENT_BENCHMARK + .write() + .unwrap_or_else(PoisonError::into_inner) = None; +} + +/// Labels each span with the benchmark running when it starts. +#[derive(Debug)] +struct BenchmarkLabelProcessor; + +impl SpanProcessor for BenchmarkLabelProcessor { + fn on_start(&self, span: &mut Span, _cx: &Context) { + let current_benchmark = CURRENT_BENCHMARK + .read() + .unwrap_or_else(PoisonError::into_inner) + .clone(); + if let Some(uri) = current_benchmark { + span.set_attribute(KeyValue::new(BENCHMARK_URI_ATTRIBUTE, uri)); + } + } + + fn on_end(&self, _span: SpanData) {} + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + Ok(()) + } +} + fn build_filter() -> EnvFilter { let user_directives = std::env::var(FILTER_ENV).unwrap_or_else(|_| DEFAULT_FILTER.to_owned()); let directives = std::iter::once(user_directives.as_str()) @@ -134,3 +184,59 @@ fn service_name() -> String { fn is_cargo_hash(s: &str) -> bool { s.len() == 16 && s.chars().all(|c| c.is_ascii_hexdigit()) } + +#[cfg(test)] +mod tests { + use super::*; + use opentelemetry::trace::Tracer as _; + use std::sync::Mutex; + + #[derive(Debug, Clone, Default)] + struct RecordingExporter(Arc>>); + + impl opentelemetry_sdk::trace::SpanExporter for RecordingExporter { + async fn export(&self, batch: Vec) -> OTelSdkResult { + self.0.lock().unwrap().extend(batch); + Ok(()) + } + } + + fn benchmark_uri(span: &SpanData) -> Option { + span.attributes + .iter() + .find(|attribute| attribute.key.as_str() == BENCHMARK_URI_ATTRIBUTE) + .map(|attribute| attribute.value.to_string()) + } + + #[test] + fn labels_the_spans_started_in_a_benchmark() { + let exporter = RecordingExporter::default(); + let provider = SdkTracerProvider::builder() + .with_span_processor(BenchmarkLabelProcessor) + .with_simple_exporter(exporter.clone()) + .build(); + let tracer = provider.tracer("test"); + + tracer.start("before").end(); + enter_benchmark("bench.rs::a"); + tracer.start("inside").end(); + exit_benchmark(); + tracer.start("after").end(); + + let labels = exporter + .0 + .lock() + .unwrap() + .iter() + .map(|span| (span.name.to_string(), benchmark_uri(span))) + .collect::>(); + assert_eq!( + labels, + [ + ("before".into(), None), + ("inside".into(), Some("bench.rs::a".into())), + ("after".into(), None), + ] + ); + } +} diff --git a/crates/divan_compat/divan_fork/src/divan.rs b/crates/divan_compat/divan_fork/src/divan.rs index 3a78d6c1..bbb347f2 100644 --- a/crates/divan_compat/divan_fork/src/divan.rs +++ b/crates/divan_compat/divan_fork/src/divan.rs @@ -296,8 +296,13 @@ impl Divan { .start_leaf(&format!("t={thread_count}"), is_last_thread_count); } + let (bench_name, uri) = + codspeed::bench_name_and_uri(&bench_entry, bench_display_name); + let mut bench_context = BenchContext::new(shared_context, options, thread_count); + ::codspeed::benchmark_scope::enter(&uri); with_bencher(Bencher::new(&mut bench_context)); + ::codspeed::benchmark_scope::exit(); if !bench_context.did_run { eprintln!( @@ -310,11 +315,7 @@ impl Divan { if should_compute_stats { let stats = bench_context.compute_stats(); - codspeed::collect_walltime_results( - &bench_context, - &bench_entry, - bench_display_name, - ); + codspeed::collect_walltime_results(&bench_context, bench_name, uri); tree_painter.borrow_mut().finish_leaf( is_last_thread_count, &stats, @@ -359,70 +360,73 @@ mod codspeed { use crate::bench::BenchContext; use crate::entry::AnyBenchEntry; - pub(crate) fn collect_walltime_results( - bench_context: &BenchContext, + pub(crate) fn bench_name_and_uri( bench_entry: &AnyBenchEntry, closure_bench_display_name: &str, - ) { + ) -> (String, String) { // WARNING: Keep URI generation in sync with `codspeed-divan-compat::uri::generate` // Not worth doing the work of actually using the same code since this fork is temporary - let (bench_name, uri) = { - let bench_function_name = bench_entry.meta().display_name; + let bench_function_name = bench_entry.meta().display_name; - let (bench_type_name, bench_arg_name) = { - let bench_function_or_type_name = bench_entry.display_name().to_string(); + let (bench_type_name, bench_arg_name) = { + let bench_function_or_type_name = bench_entry.display_name().to_string(); - let type_name = if bench_function_or_type_name == bench_function_name { - None - } else { - Some(bench_function_or_type_name) - }; - - let arg_name = match type_name.as_ref() { - None => { - if closure_bench_display_name == bench_function_name { - None - } else { - Some(closure_bench_display_name) - } + let type_name = if bench_function_or_type_name == bench_function_name { + None + } else { + Some(bench_function_or_type_name) + }; + + let arg_name = match type_name.as_ref() { + None => { + if closure_bench_display_name == bench_function_name { + None + } else { + Some(closure_bench_display_name) } - Some(type_name) => { - if closure_bench_display_name == type_name { - None - } else { - Some(closure_bench_display_name) - } + } + Some(type_name) => { + if closure_bench_display_name == type_name { + None + } else { + Some(closure_bench_display_name) } - }; - - (type_name, arg_name) + } }; - let mut bench_name = bench_function_name.to_string(); + (type_name, arg_name) + }; - match (bench_type_name, bench_arg_name) { - (None, None) => {} - (Some(type_name), None) => { - bench_name.push_str(format!("[{type_name}]").as_str()); - } - (None, Some(arg_name)) => { - bench_name.push_str(format!("[{arg_name}]").as_str()); - } - (Some(type_name), Some(arg_name)) => { - bench_name.push_str(format!("[{type_name}, {arg_name}]").as_str()); - } - } + let mut bench_name = bench_function_name.to_string(); - let file = bench_entry.meta().location.file; - let mut module_path = - bench_entry.meta().module_path_components().skip(1).collect::>().join("::"); - if !module_path.is_empty() { - module_path.push_str("::"); + match (bench_type_name, bench_arg_name) { + (None, None) => {} + (Some(type_name), None) => { + bench_name.push_str(format!("[{type_name}]").as_str()); } - let uri = format!("{file}::{module_path}{bench_name}"); - (bench_name, uri) - }; + (None, Some(arg_name)) => { + bench_name.push_str(format!("[{arg_name}]").as_str()); + } + (Some(type_name), Some(arg_name)) => { + bench_name.push_str(format!("[{type_name}, {arg_name}]").as_str()); + } + } + let file = bench_entry.meta().location.file; + let mut module_path = + bench_entry.meta().module_path_components().skip(1).collect::>().join("::"); + if !module_path.is_empty() { + module_path.push_str("::"); + } + let uri = format!("{file}::{module_path}{bench_name}"); + (bench_name, uri) + } + + pub(crate) fn collect_walltime_results( + bench_context: &BenchContext, + bench_name: String, + uri: String, + ) { let iter_per_round = bench_context.samples.sample_size; let iters_per_round = vec![iter_per_round as u128; bench_context.samples.time_samples.len()]; From 6c3f81a3062b86e0f0476d1b97bf5954a59088cb Mon Sep 17 00:00:00 2001 From: Luc Varoqui Date: Wed, 14 Oct 2026 18:20:08 +0200 Subject: [PATCH 4/4] feat(codspeed): hold exported spans until the benchmark ends The batch span processor exports on a 5s timer and whenever 512 spans are queued, so exports could run inside the measured section, and it dropped every span past a 2048-span queue. Replace it with a processor holding the ended spans in memory, exported when the benchmark scope exits, after the measured section and before the benchmark is reported as executed, and on shutdown. Exports are sent in requests of 4096 spans. Past 1,000,000 held spans, spans are dropped with a warning, to bound the memory of a benchmark emitting spans on every iteration. Refs COD-3862 Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 + crates/codspeed/Cargo.toml | 2 + crates/codspeed/src/benchmark_scope.rs | 4 +- crates/codspeed/src/otel.rs | 115 ++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38f56e02..8a88ff29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,6 +581,7 @@ dependencies = [ "anyhow", "cc", "colored", + "futures-executor", "getrandom 0.4.3", "glob", "lazy_static", diff --git a/crates/codspeed/Cargo.toml b/crates/codspeed/Cargo.toml index 460b87e0..c5562137 100644 --- a/crates/codspeed/Cargo.toml +++ b/crates/codspeed/Cargo.toml @@ -30,6 +30,7 @@ statrs = { version = "0.18.0", default-features = false } opentelemetry = { version = "0.33", default-features = false, features = ["trace"], optional = true } opentelemetry_sdk = { version = "0.33", default-features = false, features = ["trace"], optional = true } +futures-executor = { version = "0.3", default-features = false, features = ["std"], optional = true } opentelemetry-otlp = { version = "0.33", default-features = false, features = [ "trace", "http-proto", @@ -47,6 +48,7 @@ lazy_static = { version = "1.1", optional = true } [features] otel = [ + "dep:futures-executor", "dep:opentelemetry", "dep:opentelemetry_sdk", "dep:opentelemetry-otlp", diff --git a/crates/codspeed/src/benchmark_scope.rs b/crates/codspeed/src/benchmark_scope.rs index 93fdd15f..83bf41c9 100644 --- a/crates/codspeed/src/benchmark_scope.rs +++ b/crates/codspeed/src/benchmark_scope.rs @@ -12,9 +12,9 @@ pub fn enter(uri: &str) { let _ = uri; } -/// Marks the end of the current benchmark. +/// Marks the end of the current benchmark and exports the spans held so far. /// -/// Call it outside the measured section. +/// Call it outside the measured section, before the benchmark is reported as executed. pub fn exit() { #[cfg(feature = "otel")] crate::otel::exit_benchmark(); diff --git a/crates/codspeed/src/otel.rs b/crates/codspeed/src/otel.rs index 52ad9270..55498dd3 100644 --- a/crates/codspeed/src/otel.rs +++ b/crates/codspeed/src/otel.rs @@ -1,8 +1,13 @@ //! Export of the `tracing` spans and events emitted by benchmarks over OTLP/HTTP. //! //! Everything is a no-op unless [`ENDPOINT_ENV`] is set. +//! +//! Ended spans are held in memory and only exported when a benchmark scope exits (see +//! [`crate::benchmark_scope`]) or the exporter shuts down, so no export runs while a benchmark is +//! measured. -use std::sync::{Arc, PoisonError, RwLock}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, OnceLock, PoisonError, RwLock}; use std::time::Duration; use opentelemetry::trace::{Span as _, TracerProvider as _}; @@ -25,8 +30,17 @@ const DEFAULT_FILTER: &str = "info"; /// Span attribute holding the URI of the benchmark running when the span started. pub const BENCHMARK_URI_ATTRIBUTE: &str = "codspeed.benchmark.uri"; +/// Ended spans held in memory between two exports, past which spans are dropped. Bounds the +/// memory used by a benchmark emitting spans on every iteration. +const MAX_HELD_SPANS: usize = 1_000_000; + +/// Spans sent per export request. +const EXPORT_BATCH_SIZE: usize = 4_096; + static CURRENT_BENCHMARK: RwLock>> = RwLock::new(None); +static PROVIDER: OnceLock = OnceLock::new(); + /// Spans emitted by the exporter itself would be exported in turn, in an endless loop. const EXPORTER_FILTER_DIRECTIVES: &[&str] = &[ "h2=off", @@ -64,6 +78,7 @@ where S: Subscriber + for<'span> LookupSpan<'span>, { let provider = build_provider()?; + let _ = PROVIDER.set(provider.clone()); let layer = tracing_opentelemetry::layer() .with_tracer(provider.tracer("codspeed")) .with_filter(build_filter()); @@ -109,7 +124,7 @@ fn build_provider() -> Option { Some( SdkTracerProvider::builder() .with_span_processor(BenchmarkLabelProcessor) - .with_batch_exporter(exporter) + .with_span_processor(HoldingSpanProcessor::new(exporter)) .with_resource( Resource::builder() .with_service_name(service_name()) @@ -129,6 +144,12 @@ pub(crate) fn exit_benchmark() { *CURRENT_BENCHMARK .write() .unwrap_or_else(PoisonError::into_inner) = None; + + if let Some(provider) = PROVIDER.get() { + if let Err(error) = provider.force_flush() { + eprintln!("Warning: failed to export OpenTelemetry spans: {error}"); + } + } } /// Labels each span with the benchmark running when it starts. @@ -157,6 +178,73 @@ impl SpanProcessor for BenchmarkLabelProcessor { } } +/// Holds the ended spans until a flush. +#[derive(Debug)] +struct HoldingSpanProcessor { + exporter: E, + spans: Mutex>, + dropped_spans: AtomicUsize, +} + +impl HoldingSpanProcessor { + fn new(exporter: E) -> Self { + Self { + exporter, + spans: Mutex::new(Vec::new()), + dropped_spans: AtomicUsize::new(0), + } + } +} + +impl HoldingSpanProcessor { + /// Exports in batches, carrying on past a failed batch. + fn export(&self, spans: Vec) -> OTelSdkResult { + let mut result = Ok(()); + let mut spans = spans.into_iter().peekable(); + while spans.peek().is_some() { + let batch = spans.by_ref().take(EXPORT_BATCH_SIZE).collect(); + if let Err(error) = futures_executor::block_on(self.exporter.export(batch)) { + result = Err(error); + } + } + result + } +} + +impl SpanProcessor for HoldingSpanProcessor { + fn on_start(&self, _span: &mut Span, _cx: &Context) {} + + fn on_end(&self, span: SpanData) { + let mut spans = self.spans.lock().unwrap_or_else(PoisonError::into_inner); + if spans.len() < MAX_HELD_SPANS { + spans.push(span); + } else { + self.dropped_spans.fetch_add(1, Ordering::Relaxed); + } + } + + fn force_flush(&self) -> OTelSdkResult { + let spans = std::mem::take(&mut *self.spans.lock().unwrap_or_else(PoisonError::into_inner)); + let dropped_spans = self.dropped_spans.swap(0, Ordering::Relaxed); + if dropped_spans > 0 { + eprintln!( + "Warning: dropped {dropped_spans} OpenTelemetry spans, past the {MAX_HELD_SPANS} held \ + per benchmark" + ); + } + self.export(spans) + } + + fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { + self.force_flush() + .and(self.exporter.shutdown_with_timeout(timeout)) + } + + fn set_resource(&mut self, resource: &Resource) { + self.exporter.set_resource(resource); + } +} + fn build_filter() -> EnvFilter { let user_directives = std::env::var(FILTER_ENV).unwrap_or_else(|_| DEFAULT_FILTER.to_owned()); let directives = std::iter::once(user_directives.as_str()) @@ -189,13 +277,14 @@ fn is_cargo_hash(s: &str) -> bool { mod tests { use super::*; use opentelemetry::trace::Tracer as _; - use std::sync::Mutex; + /// Records the exported spans, and the size of each batch. #[derive(Debug, Clone, Default)] - struct RecordingExporter(Arc>>); + struct RecordingExporter(Arc>>, Arc>>); impl opentelemetry_sdk::trace::SpanExporter for RecordingExporter { async fn export(&self, batch: Vec) -> OTelSdkResult { + self.1.lock().unwrap().push(batch.len()); self.0.lock().unwrap().extend(batch); Ok(()) } @@ -239,4 +328,22 @@ mod tests { ] ); } + + #[test] + fn holds_spans_until_flushed() { + let exporter = RecordingExporter::default(); + let provider = SdkTracerProvider::builder() + .with_span_processor(HoldingSpanProcessor::new(exporter.clone())) + .build(); + let tracer = provider.tracer("test"); + + for _ in 0..=EXPORT_BATCH_SIZE { + tracer.start("span").end(); + } + assert!(exporter.0.lock().unwrap().is_empty()); + + provider.force_flush().unwrap(); + assert_eq!(exporter.0.lock().unwrap().len(), EXPORT_BATCH_SIZE + 1); + assert_eq!(*exporter.1.lock().unwrap(), [EXPORT_BATCH_SIZE, 1]); + } }