From 9e7359787ca4d2316e3128927d054e2904010c77 Mon Sep 17 00:00:00 2001 From: Luc Varoqui Date: Wed, 14 Oct 2026 12:09:26 +0200 Subject: [PATCH 1/6] feat(divan): export benchmark tracing spans over OTLP behind a telemetry feature Add an opt-in `telemetry` 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_TELEMETRY_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::telemetry::layer()` into their own subscriber instead. The telemetry dependencies stay within MSRV and minimal versions. `reqwest` pulls `idna_adapter` 1.2.2 and the `icu_*` 2.x crates, which require rustc 1.88: they are resolved 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: an optional direct `lazy_static = "1.1"` dependency, enabled by the `telemetry` feature, raises the floor. Closes COD-3862 Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1017 +++++++++++++++-- crates/codspeed/Cargo.toml | 30 + crates/codspeed/src/lib.rs | 2 + crates/codspeed/src/telemetry.rs | 137 +++ crates/divan_compat/Cargo.toml | 4 + crates/divan_compat/README.md | 36 + crates/divan_compat/examples/Cargo.toml | 7 +- .../examples/benches/telemetry.rs | 41 + crates/divan_compat/src/lib.rs | 12 + 9 files changed, 1176 insertions(+), 110 deletions(-) create mode 100644 crates/codspeed/src/telemetry.rs create mode 100644 crates/divan_compat/examples/benches/telemetry.rs diff --git a/Cargo.lock b/Cargo.lock index 8471bc53..7ca15c9e 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" @@ -573,7 +566,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -591,11 +584,18 @@ dependencies = [ "colored", "getrandom 0.4.3", "glob", + "lazy_static", "nix", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "serde", "serde_json", "statrs", "tempfile", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", ] [[package]] @@ -642,7 +642,7 @@ dependencies = [ "oorandom", "plotters", "quickcheck", - "rand", + "rand 0.8.5", "rayon", "regex", "serde", @@ -675,6 +675,7 @@ dependencies = [ "fastrand 2.3.0", "num-bigint", "num-traits", + "tracing", ] [[package]] @@ -686,7 +687,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -715,7 +716,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]] @@ -801,6 +802,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" @@ -809,7 +821,7 @@ checksum = "8dc51d98e636f5e3b0759a39257458b22619cac7e96d932da6eeb052891bb67c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -832,12 +844,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]] @@ -908,6 +920,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" @@ -990,6 +1011,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" @@ -1017,6 +1049,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1038,20 +1071,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" @@ -1105,6 +1144,233 @@ 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 = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "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 = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "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" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1135,6 +1401,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" @@ -1178,10 +1450,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", ] @@ -1194,6 +1468,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" @@ -1222,15 +1502,30 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[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" @@ -1247,12 +1542,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]] @@ -1261,7 +1558,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", @@ -1301,15 +1598,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" @@ -1322,12 +1610,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" @@ -1410,6 +1774,12 @@ 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 = "ppv-lite86" version = "0.2.20" @@ -1467,13 +1837,36 @@ 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]] @@ -1485,6 +1878,12 @@ 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" @@ -1498,8 +1897,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]] @@ -1509,7 +1918,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]] @@ -1521,6 +1940,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" @@ -1591,6 +2019,37 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = "rstest" version = "0.27.0" @@ -1616,16 +2075,10 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn", + "syn 2.0.96", "unicode-ident", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustc_version" version = "0.4.1" @@ -1655,13 +2108,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" @@ -1714,7 +2173,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -1739,6 +2198,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" @@ -1763,6 +2231,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" @@ -1807,6 +2281,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" @@ -1834,6 +2324,48 @@ 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.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" +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" @@ -1889,7 +2421,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.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", ] [[package]] @@ -1904,12 +2455,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]] @@ -1976,27 +2530,147 @@ 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 = "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 = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +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" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" @@ -2004,11 +2678,17 @@ 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" @@ -2035,6 +2715,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" @@ -2042,27 +2731,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", ] @@ -2080,9 +2766,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", @@ -2090,22 +2776,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" @@ -2117,6 +2806,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" @@ -2148,6 +2847,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" @@ -2166,6 +2871,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" @@ -2305,6 +3019,48 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +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.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure 0.13.2", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -2323,5 +3079,48 @@ 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 0.14.0", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e3c6377872d72510393f688a555d7097b0f741995c7a00f0407f786dd486b2d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", ] diff --git a/crates/codspeed/Cargo.toml b/crates/codspeed/Cargo.toml index 59a5ff0a..2a141f6b 100644 --- a/crates/codspeed/Cargo.toml +++ b/crates/codspeed/Cargo.toml @@ -27,6 +27,36 @@ 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 } +# Unused directly: `sharded-slab`, a dependency of `tracing-subscriber`, declares +# `lazy_static = "1"` but fails to build with 1.0, which minimal-versions resolution +# (`-Z minimal-versions`) picks. Requiring 1.1 here raises that floor. +lazy_static = { version = "1.1", optional = true } + +[features] +telemetry = [ + "dep:opentelemetry", + "dep:opentelemetry_sdk", + "dep:opentelemetry-otlp", + "dep:tracing", + "dep:tracing-opentelemetry", + "dep:tracing-subscriber", + "dep:lazy_static", +] + [[bench]] name = "native" harness = false diff --git a/crates/codspeed/src/lib.rs b/crates/codspeed/src/lib.rs index a4bc1c12..683018b7 100644 --- a/crates/codspeed/src/lib.rs +++ b/crates/codspeed/src/lib.rs @@ -6,5 +6,7 @@ pub mod isolation; mod macros; mod measurement; mod request; +#[cfg(feature = "telemetry")] +pub mod telemetry; pub mod utils; pub mod walltime_results; diff --git a/crates/codspeed/src/telemetry.rs b/crates/codspeed/src/telemetry.rs new file mode 100644 index 00000000..a1ce1170 --- /dev/null +++ b/crates/codspeed/src/telemetry.rs @@ -0,0 +1,137 @@ +//! Export of the `tracing` spans and events emitted by benchmarks over OTLP/HTTP. +//! +//! Everything is a no-op unless `CODSPEED_OTEL_ENDPOINT` 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`. +const ENDPOINT_ENV: &str = "CODSPEED_OTEL_ENDPOINT"; + +/// `EnvFilter` directives selecting the exported spans and events. Defaults to `info`. +const FILTER_ENV: &str = "CODSPEED_TELEMETRY_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 TelemetryGuard { + provider: SdkTracerProvider, +} + +impl Drop for TelemetryGuard { + 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 +/// `CODSPEED_OTEL_ENDPOINT`, 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, TelemetryGuard)> +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, TelemetryGuard { provider })) +} + +/// Installs a global `tracing` subscriber exporting spans and events to the receiver at +/// `CODSPEED_OTEL_ENDPOINT`, 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 ffaeeefe..859a2f21 100644 --- a/crates/divan_compat/Cargo.toml +++ b/crates/divan_compat/Cargo.toml @@ -24,6 +24,10 @@ codspeed-divan-compat-macros = { version = "=6.0.0", 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` +telemetry = ["codspeed/telemetry"] + [dev-dependencies] tokio = { version = "1.39", features = ["rt-multi-thread"] } diff --git a/crates/divan_compat/README.md b/crates/divan_compat/README.md index dc448a89..98011bdb 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 `telemetry` 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 = ["telemetry"] } +``` + +- `CODSPEED_OTEL_ENDPOINT`: base URL of the OTLP/HTTP receiver, e.g. `http://localhost:4318`. + Nothing is exported when it is unset. +- `CODSPEED_TELEMETRY_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 (telemetry_layer, _telemetry_guard) = divan::telemetry::layer().unzip(); + tracing_subscriber::registry() + .with(telemetry_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/examples/Cargo.toml b/crates/divan_compat/examples/Cargo.toml index 40c57f3c..66894fde 100644 --- a/crates/divan_compat/examples/Cargo.toml +++ b/crates/divan_compat/examples/Cargo.toml @@ -8,10 +8,11 @@ license = "MIT OR Apache-2.0" [dependencies] -divan = { package = "codspeed-divan-compat", path = ".." } +divan = { package = "codspeed-divan-compat", path = "..", features = ["telemetry"] } fastrand = "2.3.0" num-bigint = { version = "0.4", optional = true } num-traits = { version = "0.2", optional = true } +tracing = "0.1" [features] default = ["big-math"] @@ -48,3 +49,7 @@ harness = false [[bench]] name = "alloc" harness = false + +[[bench]] +name = "telemetry" +harness = false diff --git a/crates/divan_compat/examples/benches/telemetry.rs b/crates/divan_compat/examples/benches/telemetry.rs new file mode 100644 index 00000000..a6b5727c --- /dev/null +++ b/crates/divan_compat/examples/benches/telemetry.rs @@ -0,0 +1,41 @@ +//! Benchmarks emitting telemetry in different ways: `tracing` spans, or nothing at all. + +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() +} + +/// Nested `tracing` spans, with events in them. +#[divan::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) +} + +/// No telemetry at all. +#[divan::bench] +fn no_telemetry() -> u32 { + std::hint::black_box([1, 2, 3]).iter().sum() +} + +fn main() { + divan::main(); +} diff --git a/crates/divan_compat/src/lib.rs b/crates/divan_compat/src/lib.rs index b977abfa..dfca90c3 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 = "telemetry")] +pub use codspeed::telemetry; #[cfg(not(codspeed))] mod compat_divan { @@ -19,3 +21,13 @@ mod compat_divan { } pub use compat_divan::*; + +/// Runs all registered benchmarks. +/// +/// With the `telemetry` feature, spans and events are exported for the duration of the run. +pub fn main() { + #[cfg(feature = "telemetry")] + let _telemetry_guard = telemetry::init(); + + compat_divan::main(); +} From ca80c59ca0ac17ffb2c868b3e9bcba81cdaf1581 Mon Sep 17 00:00:00 2001 From: Luc Varoqui Date: Fri, 16 Oct 2026 12:22:33 +0200 Subject: [PATCH 2/6] feat(codspeed): export tracing events as OTLP logs Events were only recorded as span events, so a receiver got no logs signal and events emitted outside any span were lost. Export them as log records to `/v1/logs` instead, through the SDK batch log processor, carrying the trace and span IDs of the span they are emitted in. Spans no longer carry the events, which would otherwise be exported twice. The log bridge sets the event name of every record to the name of its `tracing` callsite, which `tracing` generates as `event :` for an event given no `name:`, and `log event` for a record of the `log` crate. In the OpenTelemetry logs data model a record with an event name is an event, so a log processor clears these generated names: only the events named at their callsite keep one. It compares the name with the record's own location, leaving any other name as is. The bridge's `experimental_metadata_attributes` feature is enabled for that location: the `code.filepath`, `code.lineno` and `code.namespace` attributes, which keep it exported once the name is cleared. Refs COD-3862 Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 25 ++ crates/codspeed/Cargo.toml | 19 +- crates/codspeed/src/telemetry.rs | 258 ++++++++++++++++-- crates/divan_compat/README.md | 9 +- .../examples/benches/telemetry.rs | 11 +- 5 files changed, 285 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ca15c9e..fb42e64a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -587,6 +587,7 @@ dependencies = [ "lazy_static", "nix", "opentelemetry", + "opentelemetry-appender-tracing", "opentelemetry-otlp", "opentelemetry_sdk", "serde", @@ -1623,6 +1624,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "opentelemetry-appender-tracing" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01f1c53c46ce80e5fd8cb7a646f13766fd8f315a8a5b75b9bad59a08d2c5c7" +dependencies = [ + "opentelemetry", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", +] + [[package]] name = "opentelemetry-http" version = "0.33.1" @@ -2607,6 +2621,17 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-opentelemetry" version = "0.34.0" diff --git a/crates/codspeed/Cargo.toml b/crates/codspeed/Cargo.toml index 2a141f6b..85dbfea1 100644 --- a/crates/codspeed/Cargo.toml +++ b/crates/codspeed/Cargo.toml @@ -27,14 +27,26 @@ 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 = { version = "0.33", default-features = false, features = [ + "trace", + "logs", +], optional = true } +opentelemetry_sdk = { version = "0.33", default-features = false, features = [ + "trace", + "logs", +], optional = true } +opentelemetry-appender-tracing = { version = "0.33", default-features = false, features = [ + "experimental_metadata_attributes", +], optional = true } opentelemetry-otlp = { version = "0.33", default-features = false, features = [ "trace", + "logs", "http-proto", "reqwest-blocking-client", ], optional = true } -tracing = { version = "0.1", default-features = false, features = ["std"], 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", @@ -50,6 +62,7 @@ lazy_static = { version = "1.1", optional = true } telemetry = [ "dep:opentelemetry", "dep:opentelemetry_sdk", + "dep:opentelemetry-appender-tracing", "dep:opentelemetry-otlp", "dep:tracing", "dep:tracing-opentelemetry", diff --git a/crates/codspeed/src/telemetry.rs b/crates/codspeed/src/telemetry.rs index a1ce1170..05be014f 100644 --- a/crates/codspeed/src/telemetry.rs +++ b/crates/codspeed/src/telemetry.rs @@ -1,11 +1,21 @@ -//! Export of the `tracing` spans and events emitted by benchmarks over OTLP/HTTP. +//! Export of the `tracing` spans and events emitted by benchmarks over OTLP/HTTP, as traces and +//! logs respectively. //! //! Everything is a no-op unless `CODSPEED_OTEL_ENDPOINT` is set. +use std::time::Duration; + +use opentelemetry::logs::{AnyValue, LogRecord as _}; use opentelemetry::trace::TracerProvider as _; -use opentelemetry_otlp::{SpanExporter, WithExportConfig}; -use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; +use opentelemetry::InstrumentationScope; +use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; +use opentelemetry_otlp::{LogExporter, SpanExporter, WithExportConfig}; +use opentelemetry_sdk::error::OTelSdkResult; +use opentelemetry_sdk::logs::{LogProcessor, SdkLogRecord, SdkLoggerProvider}; +use opentelemetry_sdk::trace::SdkTracerProvider; +use opentelemetry_sdk::Resource; use tracing::Subscriber; +use tracing_subscriber::filter::{filter_fn, FilterExt}; use tracing_subscriber::{layer::SubscriberExt, registry::LookupSpan, EnvFilter, Layer}; /// Base URL of the OTLP/HTTP receiver, e.g. `http://localhost:4318`. @@ -29,17 +39,26 @@ const EXPORTER_FILTER_DIRECTIVES: &[&str] = &[ "tower=off", ]; -/// Flushes and shuts down the exporter when dropped. -#[must_use = "dropping the guard shuts the exporter down"] +#[derive(Debug, Clone)] +struct Providers { + tracer: SdkTracerProvider, + logger: SdkLoggerProvider, +} + +/// Flushes and shuts down the exporters when dropped. +#[must_use = "dropping the guard shuts the exporters down"] pub struct TelemetryGuard { - provider: SdkTracerProvider, + providers: Providers, } impl Drop for TelemetryGuard { fn drop(&mut self) { - if let Err(error) = self.provider.shutdown() { + if let Err(error) = self.providers.tracer.shutdown() { eprintln!("Warning: failed to flush OpenTelemetry spans: {error}"); } + if let Err(error) = self.providers.logger.shutdown() { + eprintln!("Warning: failed to flush OpenTelemetry logs: {error}"); + } } } @@ -47,16 +66,28 @@ impl Drop for TelemetryGuard { /// `CODSPEED_OTEL_ENDPOINT`, to add to a subscriber of your own. Returns `None` when /// the variable is unset. /// +/// Events are exported as log records carrying the trace and span IDs of the span they are emitted +/// in, rather than as span events. +/// /// Keep the returned guard alive until the benchmarks are done. pub fn layer() -> Option<(impl Layer, TelemetryGuard)> 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, TelemetryGuard { provider })) + let providers = build_providers()?; + Some((build_layer(&providers), TelemetryGuard { providers })) +} + +fn build_layer(providers: &Providers) -> impl Layer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + let span_layer = tracing_opentelemetry::layer() + .with_tracer(providers.tracer.tracer("codspeed")) + .with_filter(build_filter().and(filter_fn(|metadata| metadata.is_span()))); + let log_layer = OpenTelemetryTracingBridge::new(&providers.logger) + .with_filter(build_filter().and(filter_fn(|metadata| metadata.is_event()))); + span_layer.and_then(log_layer) } /// Installs a global `tracing` subscriber exporting spans and events to the receiver at @@ -74,38 +105,100 @@ pub fn init() -> Option { if tracing::subscriber::set_global_default(subscriber).is_err() { return None; } - opentelemetry::global::set_tracer_provider(guard.provider.clone()); + opentelemetry::global::set_tracer_provider(guard.providers.tracer.clone()); Some(guard) } -fn build_provider() -> Option { +fn build_providers() -> 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 endpoint = endpoint.trim_end_matches('/'); - let exporter = match SpanExporter::builder() + let exporters = SpanExporter::builder() .with_http() - .with_endpoint(traces_endpoint) + .with_endpoint(format!("{endpoint}/v1/traces")) .build() - { - Ok(exporter) => exporter, + .and_then(|span_exporter| { + let log_exporter = LogExporter::builder() + .with_http() + .with_endpoint(format!("{endpoint}/v1/logs")) + .build()?; + Ok((span_exporter, log_exporter)) + }); + let (span_exporter, log_exporter) = match exporters { + Ok(exporters) => exporters, Err(error) => { - eprintln!("Warning: failed to build the OpenTelemetry exporter: {error}"); + eprintln!("Warning: failed to build the OpenTelemetry exporters: {error}"); return None; } }; - Some( - SdkTracerProvider::builder() - .with_batch_exporter(exporter) - .with_resource( - Resource::builder() - .with_service_name(service_name()) - .build(), - ) + let resource = Resource::builder() + .with_service_name(service_name()) + .build(); + Some(Providers { + tracer: SdkTracerProvider::builder() + .with_batch_exporter(span_exporter) + .with_resource(resource.clone()) + .build(), + logger: SdkLoggerProvider::builder() + .with_log_processor(GeneratedEventNameProcessor) + .with_batch_exporter(log_exporter) + .with_resource(resource) .build(), - ) + }) +} + +/// Event name `tracing-log` gives every record of the `log` crate. +const LOG_EVENT_NAME: &str = "log event"; + +/// Clears the event names `tracing` generates for the events given none, so that only the +/// events named at their callsite are exported with a name. Their location stays in the +/// `code.*` attributes. +#[derive(Debug)] +struct GeneratedEventNameProcessor; + +impl LogProcessor for GeneratedEventNameProcessor { + fn emit(&self, record: &mut SdkLogRecord, _instrumentation: &InstrumentationScope) { + if has_generated_event_name(record) { + record.set_event_name(""); + } + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + Ok(()) + } +} + +/// Whether the event name is `log event`, or the `event :` that `tracing` generates +/// from the location of the record. +fn has_generated_event_name(record: &SdkLogRecord) -> bool { + let Some(name) = record.event_name() else { + return false; + }; + if name == LOG_EVENT_NAME { + return true; + } + let attribute = |key: &str| { + record + .attributes_iter() + .find(|(attribute_key, _)| attribute_key.as_str() == key) + .map(|(_, value)| value) + }; + let (Some(AnyValue::String(file)), Some(&AnyValue::Int(line))) = + (attribute("code.filepath"), attribute("code.lineno")) + else { + return false; + }; + name.strip_prefix("event ") + .and_then(|location| location.strip_prefix(file.as_str())) + .and_then(|location| location.strip_prefix(':')) + .is_some_and(|location| location.parse() == Ok(line)) } fn build_filter() -> EnvFilter { @@ -135,3 +228,110 @@ 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::logs::AnyValue; + use opentelemetry_sdk::error::OTelSdkResult; + use opentelemetry_sdk::logs::{LogBatch, SdkLogRecord}; + use opentelemetry_sdk::trace::SpanData; + use std::sync::{Arc, 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(()) + } + } + + #[derive(Debug, Clone, Default)] + struct RecordingLogExporter(Arc>>); + + impl opentelemetry_sdk::logs::LogExporter for RecordingLogExporter { + async fn export(&self, batch: LogBatch<'_>) -> OTelSdkResult { + let records = batch.iter().map(|(record, _)| record.clone()); + self.0.lock().unwrap().extend(records); + Ok(()) + } + } + + fn log_body(record: &SdkLogRecord) -> String { + match record.body() { + Some(AnyValue::String(body)) => body.to_string(), + other => panic!("unexpected log body {other:?}"), + } + } + + #[test] + fn exports_events_as_log_records_of_their_span() { + let span_exporter = RecordingExporter::default(); + let log_exporter = RecordingLogExporter::default(); + let providers = Providers { + tracer: SdkTracerProvider::builder() + .with_simple_exporter(span_exporter.clone()) + .build(), + logger: SdkLoggerProvider::builder() + .with_simple_exporter(log_exporter.clone()) + .build(), + }; + let subscriber = tracing_subscriber::registry().with(build_layer(&providers)); + + tracing::subscriber::with_default(subscriber, || { + let _span = tracing::info_span!("parent").entered(); + tracing::info!("inside"); + }); + + let spans = span_exporter.0.lock().unwrap(); + let [span] = spans.as_slice() else { + panic!("expected one span, got {spans:?}"); + }; + assert!(span.events.is_empty()); + let records = log_exporter.0.lock().unwrap(); + let [record] = records.as_slice() else { + panic!("expected one log record, got {records:?}"); + }; + assert_eq!(log_body(record), "inside"); + let trace_context = record.trace_context().unwrap(); + assert_eq!(trace_context.trace_id, span.span_context.trace_id()); + assert_eq!(trace_context.span_id, span.span_context.span_id()); + } + + #[test] + fn clears_the_event_names_generated_by_tracing() { + let log_exporter = RecordingLogExporter::default(); + let providers = Providers { + tracer: SdkTracerProvider::builder().build(), + logger: SdkLoggerProvider::builder() + .with_log_processor(GeneratedEventNameProcessor) + .with_simple_exporter(log_exporter.clone()) + .build(), + }; + let subscriber = tracing_subscriber::registry().with(build_layer(&providers)); + + tracing::subscriber::with_default(subscriber, || { + tracing::info!("unnamed"); + tracing::info!(name: "cache.miss", "named"); + }); + + let records = log_exporter.0.lock().unwrap(); + let names = records + .iter() + .map(|record| (log_body(record), record.event_name())) + .collect::>(); + assert_eq!( + names, + [ + ("unnamed".to_owned(), Some("")), + ("named".to_owned(), Some("cache.miss")), + ] + ); + let has_file = records[0].attributes_iter().any(|(key, value)| { + key.as_str() == "code.filepath" && value == &AnyValue::from(file!()) + }); + assert!(has_file); + } +} diff --git a/crates/divan_compat/README.md b/crates/divan_compat/README.md index 98011bdb..b666135e 100644 --- a/crates/divan_compat/README.md +++ b/crates/divan_compat/README.md @@ -75,10 +75,11 @@ Done running my_benchmark Finished running 1 benchmark suite(s) ``` -### Exporting `tracing` spans with OpenTelemetry +### Exporting `tracing` spans and logs with OpenTelemetry With the opt-in `telemetry` 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: +exports the spans emitted by your benchmarks as traces, and their events as logs, over OTLP/HTTP. +Each log record carries the trace and span IDs of the span it was emitted in: ```toml title="Cargo.toml" [dev-dependencies] @@ -90,8 +91,8 @@ divan = { package = "codspeed-divan-compat", version = "*", features = ["telemet - `CODSPEED_TELEMETRY_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. +The tracer provider is also registered as the global OpenTelemetry tracer provider, and spans and +logs 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 diff --git a/crates/divan_compat/examples/benches/telemetry.rs b/crates/divan_compat/examples/benches/telemetry.rs index a6b5727c..1d958d07 100644 --- a/crates/divan_compat/examples/benches/telemetry.rs +++ b/crates/divan_compat/examples/benches/telemetry.rs @@ -1,4 +1,5 @@ -//! Benchmarks emitting telemetry in different ways: `tracing` spans, or nothing at all. +//! Benchmarks emitting telemetry in different ways: `tracing` spans and events, or nothing at +//! all. use tracing::{debug, debug_span, info, info_span, warn}; @@ -30,6 +31,14 @@ fn parse_and_sum() -> u32 { sum(&values) } +/// A `tracing` event outside of any span. +#[divan::bench] +fn event_without_span() -> u32 { + let values = std::hint::black_box([1, 2, 3]); + info!(count = values.len(), "summed without a span"); + values.iter().sum() +} + /// No telemetry at all. #[divan::bench] fn no_telemetry() -> u32 { From caecfce31a05767414d30281f5acd6470b0ddb1e Mon Sep 17 00:00:00 2001 From: Luc Varoqui Date: Fri, 16 Oct 2026 13:26:41 +0200 Subject: [PATCH 3/6] feat(codspeed): record the monotonic time of spans and logs Spans and log records carry wall clock times, which cannot be lined up exactly with the benchmark markers taken on the monotonic clock: the wall clock can be adjusted while the benchmarks run. With instrument hooks, each span carries the times it started and ended at on the clock of the markers as its `codspeed.monotonic_start_ns` and `codspeed.monotonic_end_ns` attributes, and each log record the time it was emitted at as `codspeed.monotonic_time_ns`. Their timestamps stay the wall clock times of the SDK, as standard tools expect. The OpenTelemetry layer takes the start time of a span when the `tracing` span is created, but only starts the OpenTelemetry span when it is first entered or closed, e.g., on the first poll of an instrumented future. A layer placed after it starts the span on creation, through `get_otel_context`, to set the start then. The OpenTelemetry layer takes the end time when the span is last exited, or closed if it never was, and ends the span when it is closed. Since the SDK only adds attributes, a layer placed before it records the time of the last exit, and sets the end once, when the span is closed. The SDK batch processors are now built with a queue of 65536 rather than the default 2048, so that a benchmark emitting on every iteration doesn't overflow it. Refs COD-3862 Co-Authored-By: Claude Opus 5.5 --- crates/codspeed/src/telemetry.rs | 275 +++++++++++++++++++++++++++++-- 1 file changed, 262 insertions(+), 13 deletions(-) diff --git a/crates/codspeed/src/telemetry.rs b/crates/codspeed/src/telemetry.rs index 05be014f..0599971b 100644 --- a/crates/codspeed/src/telemetry.rs +++ b/crates/codspeed/src/telemetry.rs @@ -2,21 +2,33 @@ //! logs respectively. //! //! Everything is a no-op unless `CODSPEED_OTEL_ENDPOINT` is set. +//! +//! Ended spans and log records are exported in batches from a background thread. +//! +//! Spans and log records keep the wall clock times of the SDK. With instrument hooks, they also +//! carry their times on the clock of the benchmark markers: spans as their +//! `codspeed.monotonic_start_ns` and `codspeed.monotonic_end_ns` attributes, log records as their +//! `codspeed.monotonic_time_ns` attribute. +use std::sync::OnceLock; use std::time::Duration; use opentelemetry::logs::{AnyValue, LogRecord as _}; -use opentelemetry::trace::TracerProvider as _; -use opentelemetry::InstrumentationScope; +use opentelemetry::trace::{TraceContextExt as _, TracerProvider as _}; +use opentelemetry::{InstrumentationScope, KeyValue}; use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; use opentelemetry_otlp::{LogExporter, SpanExporter, WithExportConfig}; use opentelemetry_sdk::error::OTelSdkResult; -use opentelemetry_sdk::logs::{LogProcessor, SdkLogRecord, SdkLoggerProvider}; -use opentelemetry_sdk::trace::SdkTracerProvider; +use opentelemetry_sdk::logs::{BatchLogProcessor, LogProcessor, SdkLogRecord, SdkLoggerProvider}; +use opentelemetry_sdk::trace::{BatchSpanProcessor, SdkTracerProvider}; use opentelemetry_sdk::Resource; -use tracing::Subscriber; +use tracing::dispatcher::WeakDispatch; +use tracing::{span, Dispatch, Subscriber}; use tracing_subscriber::filter::{filter_fn, FilterExt}; -use tracing_subscriber::{layer::SubscriberExt, registry::LookupSpan, EnvFilter, Layer}; +use tracing_subscriber::layer::{Context, SubscriberExt}; +use tracing_subscriber::{registry::LookupSpan, EnvFilter, Layer}; + +use crate::instrument_hooks::InstrumentHooks; /// Base URL of the OTLP/HTTP receiver, e.g. `http://localhost:4318`. const ENDPOINT_ENV: &str = "CODSPEED_OTEL_ENDPOINT"; @@ -26,6 +38,26 @@ const FILTER_ENV: &str = "CODSPEED_TELEMETRY_FILTER"; const DEFAULT_FILTER: &str = "info"; +/// Span attribute holding the time the span started at on the clock of the benchmark markers, in +/// nanoseconds. +const MONOTONIC_START_ATTRIBUTE: &str = "codspeed.monotonic_start_ns"; + +/// Span attribute holding the time the span ended at on the clock of the benchmark markers, in +/// nanoseconds. +const MONOTONIC_END_ATTRIBUTE: &str = "codspeed.monotonic_end_ns"; + +/// Log record attribute holding the time the record was emitted at on the clock of the benchmark +/// markers, in nanoseconds. +const MONOTONIC_TIME_ATTRIBUTE: &str = "codspeed.monotonic_time_ns"; + +/// Ended spans, and log records, queued for export, past which they are dropped. Larger than the +/// SDK default of 2048, which a benchmark emitting on every iteration fills faster than the +/// exporter drains it. +const MAX_QUEUE_SIZE: usize = 1 << 16; + +/// Spans or log records sent per export request. +const EXPORT_BATCH_SIZE: usize = 4_096; + /// Spans emitted by the exporter itself would be exported in turn, in an endless loop. const EXPORTER_FILTER_DIRECTIVES: &[&str] = &[ "h2=off", @@ -82,8 +114,9 @@ fn build_layer(providers: &Providers) -> impl Layer where S: Subscriber + for<'span> LookupSpan<'span>, { - let span_layer = tracing_opentelemetry::layer() - .with_tracer(providers.tracer.tracer("codspeed")) + let span_layer = MonotonicEndLayer::default() + .and_then(tracing_opentelemetry::layer().with_tracer(providers.tracer.tracer("codspeed"))) + .and_then(MonotonicStartLayer::default()) .with_filter(build_filter().and(filter_fn(|metadata| metadata.is_span()))); let log_layer = OpenTelemetryTracingBridge::new(&providers.logger) .with_filter(build_filter().and(filter_fn(|metadata| metadata.is_event()))); @@ -137,19 +170,154 @@ fn build_providers() -> Option { let resource = Resource::builder() .with_service_name(service_name()) .build(); + let span_processor = BatchSpanProcessor::builder(span_exporter) + .with_batch_config( + opentelemetry_sdk::trace::BatchConfigBuilder::default() + .with_max_queue_size(MAX_QUEUE_SIZE) + .with_max_export_batch_size(EXPORT_BATCH_SIZE) + .build(), + ) + .build(); + let log_processor = BatchLogProcessor::builder(log_exporter) + .with_batch_config( + opentelemetry_sdk::logs::BatchConfigBuilder::default() + .with_max_queue_size(MAX_QUEUE_SIZE) + .with_max_export_batch_size(EXPORT_BATCH_SIZE) + .build(), + ) + .build(); Some(Providers { tracer: SdkTracerProvider::builder() - .with_batch_exporter(span_exporter) + .with_span_processor(span_processor) .with_resource(resource.clone()) .build(), logger: SdkLoggerProvider::builder() .with_log_processor(GeneratedEventNameProcessor) - .with_batch_exporter(log_exporter) + .with_log_processor(MonotonicTimeProcessor) + .with_log_processor(log_processor) .with_resource(resource) .build(), }) } +/// The time on the clock of the benchmark markers, `None` without instrument hooks. +fn monotonic_now() -> Option { + match InstrumentHooks::current_timestamp() { + 0 => None, + timestamp => i64::try_from(timestamp).ok(), + } +} + +/// Sets an attribute of the OpenTelemetry span of the `tracing` span `id`. Does nothing for the +/// spans the OpenTelemetry layer filters out. +fn set_span_attribute(dispatch: &OnceLock, id: &span::Id, attribute: KeyValue) { + let Some(dispatch) = dispatch.get().and_then(WeakDispatch::upgrade) else { + return; + }; + if let Some(cx) = tracing_opentelemetry::get_otel_context(id, &dispatch) { + cx.span().set_attribute(attribute); + } +} + +/// Sets the [`MONOTONIC_START_ATTRIBUTE`] of each span when it is created. +/// +/// The OpenTelemetry layer only starts the OpenTelemetry span of a `tracing` span when it is first +/// entered, or closed, while it takes its start time when the span is created. This layer starts +/// it right after its creation, to set the attribute then. It must come after the OpenTelemetry +/// layer, whose span it starts. +#[derive(Default)] +struct MonotonicStartLayer { + dispatch: OnceLock, +} + +impl Layer for MonotonicStartLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + fn on_register_dispatch(&self, subscriber: &Dispatch) { + let _ = self.dispatch.set(subscriber.downgrade()); + } + + fn on_new_span(&self, _attrs: &span::Attributes<'_>, id: &span::Id, _ctx: Context<'_, S>) { + if let Some(timestamp) = monotonic_now() { + set_span_attribute( + &self.dispatch, + id, + KeyValue::new(MONOTONIC_START_ATTRIBUTE, timestamp), + ); + } + } +} + +/// The time a span was last exited at, on the clock of the benchmark markers. +struct LastExit(i64); + +/// Sets the [`MONOTONIC_END_ATTRIBUTE`] of each span when it is closed: the time it was last +/// exited at, or the time it is closed at when it was never entered, as the OpenTelemetry layer +/// takes its end time. +/// +/// The OpenTelemetry layer ends the span when it is closed, and the SDK only adds attributes, so +/// the attribute is set once, then. This layer must come before the OpenTelemetry layer. +#[derive(Default)] +struct MonotonicEndLayer { + dispatch: OnceLock, +} + +impl Layer for MonotonicEndLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + fn on_register_dispatch(&self, subscriber: &Dispatch) { + let _ = self.dispatch.set(subscriber.downgrade()); + } + + fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) { + if let (Some(timestamp), Some(span)) = (monotonic_now(), ctx.span(id)) { + // A span entered on several threads is exited concurrently, and the exits may take + // the lock in another order than they read the clock in. + let mut extensions = span.extensions_mut(); + match extensions.get_mut::() { + Some(last_exit) => last_exit.0 = last_exit.0.max(timestamp), + None => extensions.insert(LastExit(timestamp)), + } + } + } + + fn on_close(&self, id: span::Id, ctx: Context<'_, S>) { + let last_exit = ctx + .span(&id) + .and_then(|span| span.extensions().get::().map(|exit| exit.0)); + if let Some(timestamp) = last_exit.or_else(monotonic_now) { + set_span_attribute( + &self.dispatch, + &id, + KeyValue::new(MONOTONIC_END_ATTRIBUTE, timestamp), + ); + } + } +} + +/// Sets the [`MONOTONIC_TIME_ATTRIBUTE`] of the emitted log records, which are emitted as their +/// event happens. +#[derive(Debug)] +struct MonotonicTimeProcessor; + +impl LogProcessor for MonotonicTimeProcessor { + fn emit(&self, record: &mut SdkLogRecord, _instrumentation: &InstrumentationScope) { + if let Some(timestamp) = monotonic_now() { + record.add_attribute(MONOTONIC_TIME_ATTRIBUTE, timestamp); + } + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + Ok(()) + } +} + /// Event name `tracing-log` gives every record of the `log` crate. const LOG_EVENT_NAME: &str = "log event"; @@ -232,9 +400,8 @@ fn is_cargo_hash(s: &str) -> bool { #[cfg(test)] mod tests { use super::*; - use opentelemetry::logs::AnyValue; - use opentelemetry_sdk::error::OTelSdkResult; - use opentelemetry_sdk::logs::{LogBatch, SdkLogRecord}; + use opentelemetry::Value; + use opentelemetry_sdk::logs::LogBatch; use opentelemetry_sdk::trace::SpanData; use std::sync::{Arc, Mutex}; @@ -334,4 +501,86 @@ mod tests { }); assert!(has_file); } + + fn providers( + span_exporter: &RecordingExporter, + log_exporter: &RecordingLogExporter, + ) -> Providers { + Providers { + tracer: SdkTracerProvider::builder() + .with_simple_exporter(span_exporter.clone()) + .build(), + logger: SdkLoggerProvider::builder() + .with_log_processor(MonotonicTimeProcessor) + .with_simple_exporter(log_exporter.clone()) + .build(), + } + } + + fn span_attribute(span: &SpanData, key: &str) -> i64 { + let value = span + .attributes + .iter() + .find(|attribute| attribute.key.as_str() == key) + .map(|attribute| attribute.value.clone()); + match value { + Some(Value::I64(value)) => value, + _ => panic!("no {key} in {:?}", span.attributes), + } + } + + #[cfg(all(use_instrument_hooks, any(target_os = "linux", target_os = "macos")))] + #[test] + fn records_the_monotonic_time_of_spans_and_log_records() { + let span_exporter = RecordingExporter::default(); + let log_exporter = RecordingLogExporter::default(); + let subscriber = tracing_subscriber::registry() + .with(build_layer(&providers(&span_exporter, &log_exporter))); + let now = || InstrumentHooks::current_timestamp() as i64; + let pause = || std::thread::sleep(Duration::from_millis(1)); + + let times = tracing::subscriber::with_default(subscriber, || { + let before = now(); + let span = tracing::info_span!("entered after its creation"); + let created = now(); + pause(); + let entered = span.enter(); + tracing::info!("inside"); + let logged = now(); + drop(entered); + let exited = now(); + pause(); + drop(span); + let never_entered = tracing::info_span!("never entered"); + pause(); + let closing = now(); + drop(never_entered); + let closed = now(); + [before, created, logged, exited, closing, closed] + }); + let [before, created, logged, exited, closing, closed] = times; + + let spans = span_exporter.0.lock().unwrap(); + let [span, never_entered] = &spans[..] else { + panic!("expected two spans, got {spans:?}"); + }; + // Started when created, not when entered, and ended when last exited, not when closed. + let start = span_attribute(span, MONOTONIC_START_ATTRIBUTE); + assert!(before <= start && start <= created); + let end = span_attribute(span, MONOTONIC_END_ATTRIBUTE); + assert!(logged <= end && end <= exited); + // Ended when closed. + let end = span_attribute(never_entered, MONOTONIC_END_ATTRIBUTE); + assert!(closing <= end && end <= closed); + + let records = log_exporter.0.lock().unwrap(); + let time = records[0] + .attributes_iter() + .find(|(key, _)| key.as_str() == MONOTONIC_TIME_ATTRIBUTE) + .map(|(_, value)| value.clone()); + let Some(AnyValue::Int(time)) = time else { + panic!("no monotonic time in {:?}", records[0]); + }; + assert!(created <= time && time <= logged); + } } From 55dee758426cdff383e9ce25f047542472eb01ee Mon Sep 17 00:00:00 2001 From: Luc Varoqui Date: Mon, 19 Oct 2026 16:30:43 +0200 Subject: [PATCH 4/6] feat(cargo-codspeed): compile tracing out of measured builds, add a telemetry mode `tracing` spans and events weigh on the measurements of the code they instrument. Every build for the simulation, walltime and memory modes now passes `--cfg=codspeed_tracing_off`, behind which the `codspeed` crate depends on `tracing` with `max_level_off`: features unification compiles every span and event of the build out, dependencies included. Unlike `--features tracing/max_level_off`, it doesn't require each selected package to depend on `tracing` directly. `--keep-tracing` opts out, e.g. to benchmark a `tracing` subscriber. The new `telemetry` measurement mode builds the benchmarks to collect the telemetry they emit instead of measuring them: they run once like in simulation mode (`--cfg=codspeed`), keep `tracing`, and land in `target/codspeed/telemetry`. It builds along other modes, each with its own flags, e.g. `-m walltime,telemetry`. `--keep-tracing` is rejected when telemetry is the only mode, since it implies it. The telemetry build also passes `--cfg=codspeed_telemetry`. The dependencies exporting the telemetry move to a target table on that cfg, so the other builds don't compile them, even with the `telemetry` feature: a benchmark enabling it builds 31 crates of `codspeed` instead of 157 in measured builds. They only appear in the lockfile of the projects enabling the feature, as before. Without the cfg, `telemetry::init` and `telemetry::layer` return `None`, which keeps the API of the feature. CI checks the MSRV and minimal versions, and runs the telemetry tests, with the cfg. `release_max_level_*` features still take precedence over `max_level_off` in measured builds, since debug assertions are off. Refs COD-3862 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 12 +++ crates/cargo-codspeed/src/app.rs | 18 ++++ crates/cargo-codspeed/src/build.rs | 24 +++++- crates/cargo-codspeed/src/measurement_mode.rs | 15 ++++ .../tests/tracing.in/Cargo.toml | 15 ++++ .../tracing.in/benches/tracing_example.rs | 13 +++ crates/cargo-codspeed/tests/tracing.rs | 84 +++++++++++++++++++ crates/codspeed/Cargo.toml | 20 ++++- crates/codspeed/build.rs | 1 + crates/codspeed/src/lib.rs | 5 +- crates/codspeed/src/telemetry.rs | 4 +- crates/codspeed/src/telemetry_disabled.rs | 24 ++++++ 12 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 crates/cargo-codspeed/tests/tracing.in/Cargo.toml create mode 100644 crates/cargo-codspeed/tests/tracing.in/benches/tracing_example.rs create mode 100644 crates/cargo-codspeed/tests/tracing.rs create mode 100644 crates/codspeed/src/telemetry_disabled.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8708a8f..de12fb36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,6 +95,10 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Check cospeed MSRV run: cargo msrv --path crates/codspeed verify -- cargo check --all-features --config codspeed=true + - name: Check cospeed MSRV in telemetry builds + run: cargo msrv --path crates/codspeed verify -- cargo check --all-features --config codspeed=true + env: + RUSTFLAGS: --cfg codspeed_telemetry - name: Check bencher_compat MSRV run: cargo msrv --path crates/bencher_compat verify -- cargo check --all-features --config codspeed=true - name: Check criterion_compat MSRV @@ -114,6 +118,10 @@ jobs: - run: rustup toolchain install nightly --profile minimal - run: cargo +nightly update -Zminimal-versions -p codspeed --recursive - run: cargo check -p codspeed --all-features --config codspeed=true + - name: Check in telemetry builds + run: cargo check -p codspeed --all-features --config codspeed=true + env: + RUSTFLAGS: --cfg codspeed_telemetry tests-without-cargo-codspeed: runs-on: ubuntu-latest @@ -127,6 +135,10 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: cargo nextest run --workspace --exclude cargo-codspeed + - name: Run the telemetry tests, only built in telemetry builds + run: cargo nextest run -p codspeed --features telemetry + env: + RUSTFLAGS: --cfg codspeed_telemetry test-cargo-codspeed: runs-on: ubuntu-latest diff --git a/crates/cargo-codspeed/src/app.rs b/crates/cargo-codspeed/src/app.rs index 6c309a10..971437d9 100644 --- a/crates/cargo-codspeed/src/app.rs +++ b/crates/cargo-codspeed/src/app.rs @@ -36,7 +36,19 @@ impl Cli { offline, frozen, measurement_mode, + keep_tracing, } => { + let is_telemetry_only = !measurement_mode.is_empty() + && measurement_mode + .iter() + .all(|m| m == &MeasurementMode::Telemetry); + if is_telemetry_only && keep_tracing { + bail!( + "`--keep-tracing` has no effect in telemetry mode, which always keeps \ + `tracing`" + ); + } + let passthrough_flags = { let mut passthrough_flags = Vec::new(); if all_features { @@ -89,6 +101,7 @@ impl Cli { profile: profile.clone(), quiet: self.quiet, build_mode, + keep_tracing, passthrough_flags: passthrough_flags.clone(), }, )?; @@ -197,6 +210,11 @@ enum Commands { env = "CODSPEED_RUNNER_MODE" )] measurement_mode: Vec, + + /// Keep `tracing` spans and events compiled in. By default, the `max_level_off` feature of + /// `tracing` compiles them out of every mode but `telemetry`. + #[arg(long, help_heading = COMPILATION_HELP)] + keep_tracing: bool, }, /// Run the previously built benchmarks Run { diff --git a/crates/cargo-codspeed/src/build.rs b/crates/cargo-codspeed/src/build.rs index c99ad51e..58efc3cf 100644 --- a/crates/cargo-codspeed/src/build.rs +++ b/crates/cargo-codspeed/src/build.rs @@ -9,11 +9,17 @@ use cargo_metadata::{camino::Utf8PathBuf, Message, Metadata, TargetKind}; use std::collections::HashMap; use std::process::{exit, Command, Stdio}; +/// Set by `cargo-codspeed` in the builds `tracing` is compiled out of, see the `codspeed` crate +/// manifest +const TRACING_OFF_CFG: &str = "codspeed_tracing_off"; +const TELEMETRY_CFG: &str = "codspeed_telemetry"; + struct BuildOptions<'a> { bench_target_filters: BenchTargetFilters, package_filters: PackageFilters, features: &'a Option>, profile: &'a str, + keep_tracing: bool, passthrough_flags: &'a Vec, } @@ -31,6 +37,8 @@ pub struct BuildConfig { pub profile: String, pub quiet: bool, pub build_mode: BuildMode, + /// Keep `tracing` compiled in when building for a measurement mode + pub keep_tracing: bool, pub passthrough_flags: Vec, } @@ -226,10 +234,23 @@ See `cargo codspeed build --help` for more information."); ]; // Add the codspeed cfg flag if the benchmark should only run once - if build_mode == BuildMode::Analysis { + if build_mode.runs_once() { flags.push("--cfg=codspeed".to_owned()); } + // Compile `tracing` out of measured benchmarks, so that it doesn't weigh on the + // measurements: the `codspeed` crate enables the `max_level_off` feature of `tracing` + // behind this cfg. Only the telemetry build keeps it, to export what it records. + if build_mode != BuildMode::Telemetry && !self.keep_tracing { + flags.push(format!("--cfg={TRACING_OFF_CFG}")); + } + + // Behind this cfg, the `codspeed` crate compiles the export of the telemetry, only needed + // in the telemetry build. + if build_mode == BuildMode::Telemetry { + flags.push(format!("--cfg={TELEMETRY_CFG}")); + } + match std::env::var("RUSTFLAGS") { Result::Ok(existing_rustflags) => { // Expand already existing RUSTFLAGS env var @@ -307,6 +328,7 @@ pub fn build_benches(metadata: &Metadata, config: BuildConfig) -> Result<()> { package_filters: config.package_filters, features: &config.features, profile: &config.profile, + keep_tracing: config.keep_tracing, passthrough_flags: &config.passthrough_flags, } .build(metadata, config.quiet, build_mode)?; diff --git a/crates/cargo-codspeed/src/measurement_mode.rs b/crates/cargo-codspeed/src/measurement_mode.rs index f2635a0c..66a46006 100644 --- a/crates/cargo-codspeed/src/measurement_mode.rs +++ b/crates/cargo-codspeed/src/measurement_mode.rs @@ -7,6 +7,16 @@ pub enum BuildMode { #[default] Analysis, Walltime, + /// Runs each benchmark like [`BuildMode::Analysis`], to collect the telemetry it emits rather + /// than measure it + Telemetry, +} + +impl BuildMode { + /// Whether the benchmarks run once, behind `cfg(codspeed)`, rather than in a walltime loop + pub fn runs_once(self) -> bool { + matches!(self, BuildMode::Analysis | BuildMode::Telemetry) + } } impl fmt::Display for BuildMode { @@ -17,6 +27,7 @@ impl fmt::Display for BuildMode { match self { BuildMode::Analysis => "analysis", BuildMode::Walltime => "walltime", + BuildMode::Telemetry => "telemetry", } ) } @@ -30,6 +41,8 @@ pub enum MeasurementMode { Simulation, Walltime, Memory, + /// Collects the telemetry the benchmarks emit instead of measuring them + Telemetry, } impl From for BuildMode { @@ -37,6 +50,7 @@ impl From for BuildMode { match measurement_mode { MeasurementMode::Simulation | MeasurementMode::Memory => BuildMode::Analysis, MeasurementMode::Walltime => BuildMode::Walltime, + MeasurementMode::Telemetry => BuildMode::Telemetry, } } } @@ -50,6 +64,7 @@ impl fmt::Display for MeasurementMode { MeasurementMode::Simulation => "simulation", MeasurementMode::Walltime => "walltime", MeasurementMode::Memory => "memory", + MeasurementMode::Telemetry => "telemetry", } ) } diff --git a/crates/cargo-codspeed/tests/tracing.in/Cargo.toml b/crates/cargo-codspeed/tests/tracing.in/Cargo.toml new file mode 100644 index 00000000..bf323847 --- /dev/null +++ b/crates/cargo-codspeed/tests/tracing.in/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tracing-example" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +divan = { package = "codspeed-divan-compat", path = "../../../divan_compat" } +tracing = "0.1" + +[workspace] + +[[bench]] +name = "tracing_example" +harness = false diff --git a/crates/cargo-codspeed/tests/tracing.in/benches/tracing_example.rs b/crates/cargo-codspeed/tests/tracing.in/benches/tracing_example.rs new file mode 100644 index 00000000..5d4e7d10 --- /dev/null +++ b/crates/cargo-codspeed/tests/tracing.in/benches/tracing_example.rs @@ -0,0 +1,13 @@ +#[divan::bench] +fn traced() -> u32 { + let _span = tracing::info_span!("traced").entered(); + divan::black_box(1) + 1 +} + +fn main() { + eprintln!( + "tracing static max level: {}", + tracing::level_filters::STATIC_MAX_LEVEL + ); + divan::main(); +} diff --git a/crates/cargo-codspeed/tests/tracing.rs b/crates/cargo-codspeed/tests/tracing.rs new file mode 100644 index 00000000..3e78428b --- /dev/null +++ b/crates/cargo-codspeed/tests/tracing.rs @@ -0,0 +1,84 @@ +use predicates::str::contains; + +mod helpers; +use helpers::*; + +const DIR: &str = "tests/tracing.in"; + +fn build_and_run(build_args: &[&str], run_args: &[&str], expected_level: &str) { + let dir = setup(DIR, Project::Simple); + cargo_codspeed(&dir) + .arg("build") + .args(build_args) + .assert() + .success(); + cargo_codspeed(&dir) + .arg("run") + .args(run_args) + .assert() + .success() + .stderr(contains(format!( + "tracing static max level: {expected_level}" + ))); + teardown(dir); +} + +#[test] +fn test_tracing_compiled_out_for_simulation() { + build_and_run(&[], &[], "off"); +} + +#[test] +fn test_tracing_compiled_out_for_walltime() { + build_and_run(&["-m", "walltime"], &["-m", "walltime"], "off"); +} + +#[test] +fn test_tracing_kept_with_keep_tracing() { + build_and_run(&["--keep-tracing"], &[], "trace"); +} + +#[test] +fn test_tracing_kept_for_telemetry() { + build_and_run(&["-m", "telemetry"], &["-m", "telemetry"], "trace"); +} + +#[test] +fn test_telemetry_run_without_build() { + let dir = setup(DIR, Project::Simple); + cargo_codspeed(&dir) + .args(["run", "-m", "telemetry"]) + .assert() + .failure() + .stderr(contains( + "Error: No benchmarks found for the telemetry mode. Run `cargo codspeed build -m telemetry` first.", + )); + teardown(dir); +} + +#[test] +fn test_tracing_kept_for_telemetry_built_with_other_modes() { + build_and_run(&["-m", "walltime,telemetry"], &["-m", "telemetry"], "trace"); +} + +#[test] +fn test_tracing_kept_with_keep_tracing_and_telemetry() { + build_and_run( + &["-m", "walltime,telemetry", "--keep-tracing"], + &["-m", "walltime"], + "trace", + ); +} + +#[test] +fn test_telemetry_rejects_keep_tracing() { + let dir = setup(DIR, Project::Simple); + cargo_codspeed(&dir) + .args(["build", "-m", "telemetry", "--keep-tracing"]) + .assert() + .failure() + .stderr(contains( + "Error: `--keep-tracing` has no effect in telemetry mode", + )); + teardown(dir); +} diff --git a/crates/codspeed/Cargo.toml b/crates/codspeed/Cargo.toml index 85dbfea1..293a6c1a 100644 --- a/crates/codspeed/Cargo.toml +++ b/crates/codspeed/Cargo.toml @@ -27,6 +27,16 @@ serde = { workspace = true } serde_json = { workspace = true } statrs = { version = "0.18.0", default-features = false } +tracing = { version = "0.1", default-features = false, features = [ + "std", +], optional = true } +tracing-subscriber = { version = "0.3", default-features = false, features = [ + "std", +], optional = true } + +# The export of the telemetry: `cargo-codspeed` only sets this cfg in its telemetry builds, so that +# the other builds don't compile it, even with the `telemetry` feature. +[target.'cfg(codspeed_telemetry)'.dependencies] opentelemetry = { version = "0.33", default-features = false, features = [ "trace", "logs", @@ -44,9 +54,6 @@ opentelemetry-otlp = { version = "0.33", default-features = false, features = [ "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", @@ -58,6 +65,13 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [ # (`-Z minimal-versions`) picks. Requiring 1.1 here raises that floor. lazy_static = { version = "1.1", optional = true } +# `cargo-codspeed` sets this cfg when building benchmarks for a measurement mode, +# so that features unification compiles the `tracing` spans and events of the whole build out. +[target.'cfg(codspeed_tracing_off)'.dependencies] +tracing = { version = "0.1", default-features = false, features = [ + "max_level_off", +] } + [features] telemetry = [ "dep:opentelemetry", diff --git a/crates/codspeed/build.rs b/crates/codspeed/build.rs index 81e7850f..a0f425c8 100644 --- a/crates/codspeed/build.rs +++ b/crates/codspeed/build.rs @@ -1,6 +1,7 @@ fn main() { println!("cargo:rustc-check-cfg=cfg(use_instrument_hooks)"); println!("cargo:rustc-check-cfg=cfg(codspeed)"); + println!("cargo:rustc-check-cfg=cfg(codspeed_telemetry)"); println!("cargo:rerun-if-changed=instrument-hooks/dist/core.c"); println!("cargo:rerun-if-changed=instrument-hooks/includes/core.h"); diff --git a/crates/codspeed/src/lib.rs b/crates/codspeed/src/lib.rs index 683018b7..ac656df0 100644 --- a/crates/codspeed/src/lib.rs +++ b/crates/codspeed/src/lib.rs @@ -6,7 +6,10 @@ pub mod isolation; mod macros; mod measurement; mod request; -#[cfg(feature = "telemetry")] +#[cfg(all(feature = "telemetry", codspeed_telemetry))] +pub mod telemetry; +#[cfg(all(feature = "telemetry", not(codspeed_telemetry)))] +#[path = "telemetry_disabled.rs"] pub mod telemetry; pub mod utils; pub mod walltime_results; diff --git a/crates/codspeed/src/telemetry.rs b/crates/codspeed/src/telemetry.rs index 0599971b..17a47ee2 100644 --- a/crates/codspeed/src/telemetry.rs +++ b/crates/codspeed/src/telemetry.rs @@ -3,7 +3,9 @@ //! //! Everything is a no-op unless `CODSPEED_OTEL_ENDPOINT` is set. //! -//! Ended spans and log records are exported in batches from a background thread. +//! Ended spans and log records are exported in batches from a background thread. Telemetry is +//! collected in a run of its own, whose performance is not measured, so exporting while a +//! benchmark runs doesn't matter. //! //! Spans and log records keep the wall clock times of the SDK. With instrument hooks, they also //! carry their times on the clock of the benchmark markers: spans as their diff --git a/crates/codspeed/src/telemetry_disabled.rs b/crates/codspeed/src/telemetry_disabled.rs new file mode 100644 index 00000000..f5190df2 --- /dev/null +++ b/crates/codspeed/src/telemetry_disabled.rs @@ -0,0 +1,24 @@ +//! Stands in for the export of the `tracing` spans and events in the builds that don't compile it: +//! only the telemetry builds of `cargo-codspeed` do. Nothing is exported. + +use tracing::Subscriber; +use tracing_subscriber::layer::Identity; +use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::Layer; + +/// Flushes and shuts down the exporters when dropped. +#[must_use = "dropping the guard shuts the exporters down"] +pub struct TelemetryGuard(()); + +/// Returns `None`: the layer only exports in the telemetry builds. +pub fn layer() -> Option<(impl Layer, TelemetryGuard)> +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + None::<(Identity, TelemetryGuard)> +} + +/// Returns `None`: the subscriber is only installed in the telemetry builds. +pub fn init() -> Option { + None +} From 03b5318825bc8feff381e31c9f875014ae48e0e9 Mon Sep 17 00:00:00 2001 From: Luc Varoqui Date: Thu, 22 Oct 2026 11:08:12 +0200 Subject: [PATCH 5/6] feat(codspeed): export the records of the log crate `init` installs its subscriber with `tracing::subscriber::set_global_default`, which, unlike `tracing-subscriber`'s `init`, doesn't forward the records of the `log` crate to it: benchmarks and dependencies logging with `log` exported nothing. `init` now also installs `tracing-log`'s `LogTracer`, unless a `log` logger is already set. The records go through the same `EnvFilter` as events, by their `log` target, and the bridge reads their location from their normalized metadata, so they get the right `code.*` attributes. The `log event` name they are given is already cleared. `LogTracer` takes the max level of the subscriber, which the `log` macros check before anything else. The `filter_fn` filters of the layer gave it no hint, which lifted it to `TRACE`: they now hint `TRACE`, so that the `EnvFilter` hint is kept. `layer` stays free of global side effects, and documents that a subscriber of your own needs a `LogTracer` to receive `log` records. A test runs `init`, in a binary of its own since it sets process-wide state, against an in-process OTLP/HTTP receiver, and checks the records it decodes. Like the export, it is only built with `--cfg codspeed_telemetry`. Refs COD-3862 Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 4 + crates/codspeed/Cargo.toml | 15 +++ crates/codspeed/src/telemetry.rs | 22 +++- crates/codspeed/tests/telemetry_log.rs | 124 ++++++++++++++++++ crates/divan_compat/README.md | 6 +- crates/divan_compat/examples/Cargo.toml | 1 + .../examples/benches/telemetry.rs | 13 +- 7 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 crates/codspeed/tests/telemetry_log.rs diff --git a/Cargo.lock b/Cargo.lock index fb42e64a..16f9e437 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -589,12 +589,15 @@ dependencies = [ "opentelemetry", "opentelemetry-appender-tracing", "opentelemetry-otlp", + "opentelemetry-proto", "opentelemetry_sdk", + "prost", "serde", "serde_json", "statrs", "tempfile", "tracing", + "tracing-log", "tracing-opentelemetry", "tracing-subscriber", ] @@ -674,6 +677,7 @@ version = "0.0.0" dependencies = [ "codspeed-divan-compat", "fastrand 2.3.0", + "log", "num-bigint", "num-traits", "tracing", diff --git a/crates/codspeed/Cargo.toml b/crates/codspeed/Cargo.toml index 293a6c1a..6c6afb32 100644 --- a/crates/codspeed/Cargo.toml +++ b/crates/codspeed/Cargo.toml @@ -54,6 +54,10 @@ opentelemetry-otlp = { version = "0.33", default-features = false, features = [ "http-proto", "reqwest-blocking-client", ], optional = true } +tracing-log = { version = "0.2", default-features = false, features = [ + "log-tracer", + "std", +], optional = true } tracing-opentelemetry = { version = "0.34", default-features = false, optional = true } tracing-subscriber = { version = "0.3", default-features = false, features = [ "std", @@ -79,6 +83,7 @@ telemetry = [ "dep:opentelemetry-appender-tracing", "dep:opentelemetry-otlp", "dep:tracing", + "dep:tracing-log", "dep:tracing-opentelemetry", "dep:tracing-subscriber", "dep:lazy_static", @@ -88,7 +93,17 @@ telemetry = [ name = "native" harness = false +[[test]] +name = "telemetry_log" +required-features = ["telemetry"] + [dev-dependencies] +opentelemetry-proto = { version = "0.33", default-features = false, features = [ + "gen-tonic-messages", + "logs", + "trace", +] } +prost = "0.14" tempfile = { workspace = true } [build-dependencies] diff --git a/crates/codspeed/src/telemetry.rs b/crates/codspeed/src/telemetry.rs index 17a47ee2..b73400fc 100644 --- a/crates/codspeed/src/telemetry.rs +++ b/crates/codspeed/src/telemetry.rs @@ -25,7 +25,9 @@ use opentelemetry_sdk::logs::{BatchLogProcessor, LogProcessor, SdkLogRecord, Sdk use opentelemetry_sdk::trace::{BatchSpanProcessor, SdkTracerProvider}; use opentelemetry_sdk::Resource; use tracing::dispatcher::WeakDispatch; +use tracing::level_filters::LevelFilter; use tracing::{span, Dispatch, Subscriber}; +use tracing_log::{AsLog, LogTracer}; use tracing_subscriber::filter::{filter_fn, FilterExt}; use tracing_subscriber::layer::{Context, SubscriberExt}; use tracing_subscriber::{registry::LookupSpan, EnvFilter, Layer}; @@ -103,6 +105,10 @@ impl Drop for TelemetryGuard { /// Events are exported as log records carrying the trace and span IDs of the span they are emitted /// in, rather than as span events. /// +/// Records of the `log` crate reach the subscriber only through a [`LogTracer`], which +/// `tracing-subscriber`'s `init` and `try_init` install with its default `tracing-log` feature. +/// Otherwise, call [`LogTracer::init`]. +/// /// Keep the returned guard alive until the benchmarks are done. pub fn layer() -> Option<(impl Layer, TelemetryGuard)> where @@ -116,12 +122,17 @@ fn build_layer(providers: &Providers) -> impl Layer where S: Subscriber + for<'span> LookupSpan<'span>, { + // Without a hint, `filter_fn` would lift the max level of the subscriber to `TRACE`. + let span_filter = + filter_fn(|metadata| metadata.is_span()).with_max_level_hint(LevelFilter::TRACE); + let event_filter = + filter_fn(|metadata| metadata.is_event()).with_max_level_hint(LevelFilter::TRACE); let span_layer = MonotonicEndLayer::default() .and_then(tracing_opentelemetry::layer().with_tracer(providers.tracer.tracer("codspeed"))) .and_then(MonotonicStartLayer::default()) - .with_filter(build_filter().and(filter_fn(|metadata| metadata.is_span()))); + .with_filter(build_filter().and(span_filter)); let log_layer = OpenTelemetryTracingBridge::new(&providers.logger) - .with_filter(build_filter().and(filter_fn(|metadata| metadata.is_event()))); + .with_filter(build_filter().and(event_filter)); span_layer.and_then(log_layer) } @@ -129,6 +140,9 @@ where /// `CODSPEED_OTEL_ENDPOINT`, and registers its provider as the global OpenTelemetry tracer /// provider. /// +/// Records of the `log` crate are exported as log records too, unless a `log` logger is already +/// set. +/// /// Returns `None` when the variable is unset or a global subscriber is already set. pub fn init() -> Option { if tracing::dispatcher::has_been_set() { @@ -140,6 +154,10 @@ pub fn init() -> Option { if tracing::subscriber::set_global_default(subscriber).is_err() { return None; } + // Set after the subscriber, so that `log` skips the records above its max level. + let _ = LogTracer::builder() + .with_max_level(LevelFilter::current().as_log()) + .init(); opentelemetry::global::set_tracer_provider(guard.providers.tracer.clone()); Some(guard) } diff --git a/crates/codspeed/tests/telemetry_log.rs b/crates/codspeed/tests/telemetry_log.rs new file mode 100644 index 00000000..492004b0 --- /dev/null +++ b/crates/codspeed/tests/telemetry_log.rs @@ -0,0 +1,124 @@ +//! Exports the records of the `log` crate through [`telemetry::init`], to an OTLP/HTTP receiver. +//! +//! In a test binary of its own, since `init` installs process-wide loggers. +#![cfg(codspeed_telemetry)] + +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::TcpListener; +use std::sync::mpsc; +use std::thread; + +use codspeed::telemetry; +use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; +use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest; +use opentelemetry_proto::tonic::common::v1::{any_value::Value, KeyValue}; +use opentelemetry_proto::tonic::logs::v1::SeverityNumber; +use prost::Message; +use tracing_log::log; + +/// Accepts OTLP/HTTP requests on a local port, sending the path and body of each. +fn spawn_receiver() -> (String, mpsc::Receiver<(String, Vec)>) { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let endpoint = format!("http://{}", listener.local_addr().unwrap()); + let (sender, receiver) = mpsc::channel(); + thread::spawn(move || { + for stream in listener.incoming() { + let mut stream = BufReader::new(stream.unwrap()); + let mut lines = (&mut stream).lines().map(Result::unwrap); + let path = lines.next().unwrap().split(' ').nth(1).unwrap().to_owned(); + let headers = lines + .take_while(|header| !header.is_empty()) + .collect::>(); + // The exporter sends uncompressed bodies of known length. + let content_length = headers + .iter() + .find_map(|header| { + let (name, value) = header.split_once(':')?; + name.eq_ignore_ascii_case("content-length") + .then(|| value.trim().parse::().unwrap()) + }) + .expect("a Content-Length header"); + let mut body = vec![0; content_length]; + stream.read_exact(&mut body).unwrap(); + sender.send((path, body)).unwrap(); + stream + .get_mut() + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") + .unwrap(); + } + }); + (endpoint, receiver) +} + +fn attribute(attributes: &[KeyValue], key: &str) -> Option { + let attribute = attributes.iter().find(|attribute| attribute.key == key)?; + attribute.value.clone()?.value +} + +fn string(value: &str) -> Option { + Some(Value::StringValue(value.to_owned())) +} + +#[test] +fn exports_the_records_of_the_log_crate() { + let (endpoint, requests) = spawn_receiver(); + std::env::set_var("CODSPEED_OTEL_ENDPOINT", endpoint); + std::env::remove_var("CODSPEED_TELEMETRY_FILTER"); + let guard = telemetry::init().expect("telemetry should be enabled"); + // Taken from the filter of the subscriber: the `log` macros skip the records above it. + assert_eq!(log::max_level(), log::LevelFilter::Info); + + log::debug!("filtered out by level"); + log::info!(target: "reqwest::connect", "filtered out by target"); + let span = tracing::info_span!("span").entered(); + log::info!("from log"); + let line = line!() - 1; + drop(span); + drop(guard); + + let (mut spans, mut records) = (Vec::new(), Vec::new()); + for (path, body) in requests.try_iter() { + match path.as_str() { + "/v1/traces" => spans.extend( + ExportTraceServiceRequest::decode(body.as_slice()) + .unwrap() + .resource_spans + .into_iter() + .flat_map(|resource| resource.scope_spans) + .flat_map(|scope| scope.spans), + ), + "/v1/logs" => records.extend( + ExportLogsServiceRequest::decode(body.as_slice()) + .unwrap() + .resource_logs + .into_iter() + .flat_map(|resource| resource.scope_logs) + .flat_map(|scope| scope.log_records), + ), + other => panic!("unexpected request to {other}"), + } + } + let [span] = spans.as_slice() else { + panic!("expected one span, got {spans:?}"); + }; + let [record] = records.as_slice() else { + panic!("expected one log record, got {records:?}"); + }; + let body = record.body.clone().and_then(|body| body.value); + assert_eq!(body, string("from log")); + assert_eq!(record.severity_number, SeverityNumber::Info as i32); + assert_eq!(record.severity_text, "INFO"); + assert_eq!(record.event_name, ""); + let attributes = &record.attributes; + assert_eq!(attribute(attributes, "code.filepath"), string(file!())); + assert_eq!( + attribute(attributes, "code.lineno"), + Some(Value::IntValue(line.into())) + ); + assert_eq!( + attribute(attributes, "code.namespace"), + string(module_path!()) + ); + assert_eq!(record.trace_id, span.trace_id); + assert_eq!(record.span_id, span.span_id); +} diff --git a/crates/divan_compat/README.md b/crates/divan_compat/README.md index b666135e..3f02ea83 100644 --- a/crates/divan_compat/README.md +++ b/crates/divan_compat/README.md @@ -91,12 +91,16 @@ divan = { package = "codspeed-divan-compat", version = "*", features = ["telemet - `CODSPEED_TELEMETRY_FILTER`: [`EnvFilter`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) directives selecting what is exported. Defaults to `info`. +The records of the [`log`](https://docs.rs/log) crate are exported as logs too, unless a `log` +logger is set before calling `divan::main()`. + The tracer provider is also registered as the global OpenTelemetry tracer provider, and spans and logs 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: +benchmarks are done. `tracing-subscriber`'s `init()` also forwards the records of the `log` crate +to it, with its default `tracing-log` feature: ```rust use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; diff --git a/crates/divan_compat/examples/Cargo.toml b/crates/divan_compat/examples/Cargo.toml index 66894fde..bd8ed957 100644 --- a/crates/divan_compat/examples/Cargo.toml +++ b/crates/divan_compat/examples/Cargo.toml @@ -10,6 +10,7 @@ license = "MIT OR Apache-2.0" [dependencies] divan = { package = "codspeed-divan-compat", path = "..", features = ["telemetry"] } fastrand = "2.3.0" +log = "0.4" num-bigint = { version = "0.4", optional = true } num-traits = { version = "0.2", optional = true } tracing = "0.1" diff --git a/crates/divan_compat/examples/benches/telemetry.rs b/crates/divan_compat/examples/benches/telemetry.rs index 1d958d07..7b8969e4 100644 --- a/crates/divan_compat/examples/benches/telemetry.rs +++ b/crates/divan_compat/examples/benches/telemetry.rs @@ -1,5 +1,5 @@ -//! Benchmarks emitting telemetry in different ways: `tracing` spans and events, or nothing at -//! all. +//! Benchmarks emitting telemetry in different ways: `tracing` spans and events, records of the +//! `log` crate, or nothing at all. use tracing::{debug, debug_span, info, info_span, warn}; @@ -39,6 +39,15 @@ fn event_without_span() -> u32 { values.iter().sum() } +/// Records of the `log` crate, without `tracing`. +#[divan::bench] +fn log_records() -> u32 { + let values = std::hint::black_box([1, 2, 3]); + log::info!("summing {} values, logged with the log crate", values.len()); + log::debug!("filtered out by the default filter"); + values.iter().sum() +} + /// No telemetry at all. #[divan::bench] fn no_telemetry() -> u32 { From 31f47a5a5ce72999379195b4b103cb2dc7456935 Mon Sep 17 00:00:00 2001 From: Luc Varoqui Date: Thu, 22 Oct 2026 16:17:50 +0200 Subject: [PATCH 6/6] perf(codspeed): warm the telemetry up before the benchmarks run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first span of the process takes 20 to 30 µs longer than the next ones to go through the subscriber, and the first event about 10 µs longer. The first benchmark emitting them took that time, which delayed its telemetry from the start of its run. `init` now emits a span and an event once the subscriber is installed. They carry a `codspeed_warmup` field, which the processors in front of the exporters drop, and a target the filter always lets through at the `error` level, which keeps the max level `log` takes from the filter. Refs COD-3862 Co-Authored-By: Claude Opus 5.5 --- crates/codspeed/src/telemetry.rs | 84 ++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/crates/codspeed/src/telemetry.rs b/crates/codspeed/src/telemetry.rs index b73400fc..79a0a714 100644 --- a/crates/codspeed/src/telemetry.rs +++ b/crates/codspeed/src/telemetry.rs @@ -22,7 +22,9 @@ use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; use opentelemetry_otlp::{LogExporter, SpanExporter, WithExportConfig}; use opentelemetry_sdk::error::OTelSdkResult; use opentelemetry_sdk::logs::{BatchLogProcessor, LogProcessor, SdkLogRecord, SdkLoggerProvider}; -use opentelemetry_sdk::trace::{BatchSpanProcessor, SdkTracerProvider}; +use opentelemetry_sdk::trace::{ + BatchSpanProcessor, SdkTracerProvider, Span, SpanData, SpanProcessor, +}; use opentelemetry_sdk::Resource; use tracing::dispatcher::WeakDispatch; use tracing::level_filters::LevelFilter; @@ -62,6 +64,12 @@ const MAX_QUEUE_SIZE: usize = 1 << 16; /// Spans or log records sent per export request. const EXPORT_BATCH_SIZE: usize = 4_096; +/// Target of the span and event [`warm_up`] emits. +const WARMUP_TARGET: &str = "codspeed::telemetry::warmup"; + +/// Field marking the span and event [`warm_up`] emits, which are not exported. +const WARMUP_FIELD: &str = "codspeed_warmup"; + /// Spans emitted by the exporter itself would be exported in turn, in an endless loop. const EXPORTER_FILTER_DIRECTIVES: &[&str] = &[ "h2=off", @@ -159,9 +167,19 @@ pub fn init() -> Option { .with_max_level(LevelFilter::current().as_log()) .init(); opentelemetry::global::set_tracer_provider(guard.providers.tracer.clone()); + warm_up(); Some(guard) } +/// The first span and the first event of the process take tens of microseconds longer than the +/// next ones to go through the subscriber. Emits one of each, which are not exported, so that the +/// first benchmark emitting telemetry doesn't take that time. +fn warm_up() { + let span = tracing::error_span!(target: WARMUP_TARGET, "warmup", codspeed_warmup = true); + let _entered = span.enter(); + tracing::error!(target: WARMUP_TARGET, codspeed_warmup = true, "warmup"); +} + fn build_providers() -> Option { let endpoint = std::env::var(ENDPOINT_ENV) .ok() @@ -208,13 +226,13 @@ fn build_providers() -> Option { .build(); Some(Providers { tracer: SdkTracerProvider::builder() - .with_span_processor(span_processor) + .with_span_processor(WarmupFilter(span_processor)) .with_resource(resource.clone()) .build(), logger: SdkLoggerProvider::builder() .with_log_processor(GeneratedEventNameProcessor) .with_log_processor(MonotonicTimeProcessor) - .with_log_processor(log_processor) + .with_log_processor(WarmupFilter(log_processor)) .with_resource(resource) .build(), }) @@ -389,9 +407,67 @@ fn has_generated_event_name(record: &SdkLogRecord) -> bool { .is_some_and(|location| location.parse() == Ok(line)) } +/// Drops the span and event [`warm_up`] emits, before handing the others to the processor it +/// wraps. +#[derive(Debug)] +struct WarmupFilter

(P); + +impl SpanProcessor for WarmupFilter

{ + fn on_start(&self, span: &mut Span, cx: &opentelemetry::Context) { + self.0.on_start(span, cx); + } + + fn on_end(&self, span: SpanData) { + let is_warmup = span + .attributes + .iter() + .any(|attribute| attribute.key.as_str() == WARMUP_FIELD); + if !is_warmup { + self.0.on_end(span); + } + } + + fn force_flush(&self) -> OTelSdkResult { + self.0.force_flush() + } + + fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { + self.0.shutdown_with_timeout(timeout) + } + + fn set_resource(&mut self, resource: &Resource) { + self.0.set_resource(resource); + } +} + +impl LogProcessor for WarmupFilter

{ + fn emit(&self, record: &mut SdkLogRecord, instrumentation: &InstrumentationScope) { + let is_warmup = record + .attributes_iter() + .any(|(key, _)| key.as_str() == WARMUP_FIELD); + if !is_warmup { + self.0.emit(record, instrumentation); + } + } + + fn force_flush(&self) -> OTelSdkResult { + self.0.force_flush() + } + + fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { + self.0.shutdown_with_timeout(timeout) + } + + fn set_resource(&mut self, resource: &Resource) { + self.0.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()) + let warmup_directive = format!("{WARMUP_TARGET}=error"); + let directives = [user_directives.as_str(), warmup_directive.as_str()] + .into_iter() .chain(EXPORTER_FILTER_DIRECTIVES.iter().copied()) .collect::>() .join(",");