From bf2d5f204a1b5afe9e7cd4caf0fc3e706a0c610f Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 14:08:35 +0200 Subject: [PATCH 01/19] Introduce OpenTelemetry in rewatch Add optional OTLP tracing export to rewatch, controlled by the OTEL_EXPORTER_OTLP_ENDPOINT environment variable. When set, rewatch exports spans via HTTP OTLP; when unset, tracing is a no-op. Instrument key build system functions (initialize_build, incremental_build, compile, parse, clean, format, packages) with tracing spans and attributes such as module counts and package names. Restructure main.rs to support telemetry lifecycle (init/flush/shutdown) and fix show_progress to use >= LevelFilter::Info so -v/-vv don't suppress progress messages. Also print 'Finished compilation' in plain_output mode during watch full rebuilds. Cherry-picked from #8241. --- rewatch/Cargo.lock | 1063 ++++++++++++++++++++++++++++++++- rewatch/Cargo.toml | 21 +- rewatch/src/build.rs | 6 + rewatch/src/build/clean.rs | 3 + rewatch/src/build/compile.rs | 27 + rewatch/src/build/packages.rs | 4 + rewatch/src/build/parse.rs | 15 + rewatch/src/format.rs | 3 + rewatch/src/lib.rs | 1 + rewatch/src/main.rs | 192 +++--- rewatch/src/telemetry.rs | 141 +++++ rewatch/src/watcher.rs | 6 + 12 files changed, 1403 insertions(+), 79 deletions(-) create mode 100644 rewatch/src/telemetry.rs diff --git a/rewatch/Cargo.lock b/rewatch/Cargo.lock index 5ebb674c3fb..898a12bbb32 100644 --- a/rewatch/Cargo.lock +++ b/rewatch/Cargo.lock @@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.3.3", "once_cell", "version_check", "zerocopy", @@ -92,6 +92,29 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -123,6 +146,12 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cc" version = "1.2.28" @@ -278,6 +307,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.15.0" @@ -331,6 +371,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[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 = "fsevent-sys" version = "4.1.0" @@ -435,6 +484,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -447,6 +507,12 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "heck" version = "0.5.0" @@ -459,12 +525,197 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +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 = "humantime" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indicatif" version = "0.17.11" @@ -498,6 +749,22 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.16" @@ -515,6 +782,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -551,6 +827,12 @@ dependencies = [ "libc", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.174" @@ -574,12 +856,27 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[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.5" @@ -598,6 +895,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + [[package]] name = "nix" version = "0.30.1" @@ -623,7 +931,7 @@ dependencies = [ "inotify", "kqueue", "libc", - "mio", + "mio 0.8.11", "serde", "walkdir", "windows-sys 0.45.0", @@ -638,6 +946,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "num_cpus" version = "1.17.0" @@ -666,6 +983,110 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "opentelemetry" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c365a63eec4f55b7efeceb724f1336f26a9cf3427b70e59e2cd2a5b947fba96" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror", +] + +[[package]] +name = "opentelemetry-http" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad31e9de44ee3538fb9d64fe3376c1362f406162434609e79aea2a41a0af78ab" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b925a602ffb916fb7421276b86756027b37ee708f9dce2dbdcc51739f07e727" +dependencies = [ + "async-trait", + "futures-core", + "http", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "thiserror", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ee9f20bff9c984511a02f082dc8ede839e4a9bf15cc2487c8d6fea5ad850d9" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692eac490ec80f24a17828d49b40b60f5aeaccdfe6a503f939713afd22bc28df" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "once_cell", + "opentelemetry", + "percent-encoding", + "rand", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -684,6 +1105,24 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -693,6 +1132,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.40" @@ -709,13 +1171,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rayon" -version = "1.10.0" +name = "rand" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "either", - "rayon-core", + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", ] [[package]] @@ -766,6 +1258,40 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +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", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "rescript" version = "13.0.0-alpha.4" @@ -785,6 +1311,9 @@ dependencies = [ "log", "notify", "num_cpus", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "rayon", "regex", "serde", @@ -793,6 +1322,10 @@ dependencies = [ "serde_path_to_error", "sysinfo", "tempfile", + "tokio", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", ] [[package]] @@ -808,6 +1341,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.20" @@ -886,6 +1425,27 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "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 = "1.3.0" @@ -898,6 +1458,28 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -915,6 +1497,26 @@ dependencies = [ "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", +] + [[package]] name = "sysinfo" version = "0.29.11" @@ -937,7 +1539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -952,6 +1554,220 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "libc", + "mio 1.1.0", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-trait", + "base64", + "bytes", + "http", + "http-body", + "http-body-util", + "percent-encoding", + "pin-project", + "prost", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[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.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "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.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9784ed4da7d921bc8df6963f8c80a0e4ce34ba6ba76668acadd3edbd985ff3b" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[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.18" @@ -970,12 +1786,36 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -992,6 +1832,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1015,6 +1864,7 @@ checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] @@ -1032,6 +1882,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -1064,6 +1927,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -1105,6 +1978,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.45.0" @@ -1132,6 +2011,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[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.42.2" @@ -1171,13 +2068,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -1196,6 +2110,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -1214,6 +2134,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -1232,12 +2158,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -1256,6 +2194,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -1274,6 +2218,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -1292,6 +2242,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -1310,6 +2266,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -1319,6 +2281,35 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.26" @@ -1338,3 +2329,57 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rewatch/Cargo.toml b/rewatch/Cargo.toml index e80f10b40f8..66e5e81f35e 100644 --- a/rewatch/Cargo.toml +++ b/rewatch/Cargo.toml @@ -12,7 +12,12 @@ clap = { version = "4.3", features = ["derive"] } clap-verbosity-flag = "2.2" console = "0.15.5" convert_case = "0.6.0" -ctrlc = "3.4.4" +# The "termination" feature extends ctrlc's handler to also catch SIGTERM and +# SIGHUP (not just SIGINT). This is required so the telemetry guard's Drop +# runs on those signals and buffered OTLP spans get flushed before exit. +# Without it, `kill ` or a container/systemd stop would silently drop +# the last batch of traces. +ctrlc = { version = "3.4.4", features = ["termination"] } env_logger = "0.10" futures = "0.3.25" futures-timer = "3.0.2" @@ -29,6 +34,20 @@ serde_path_to_error = "0.1.16" sysinfo = "0.29.10" tempfile = "3.10.1" +# OpenTelemetry for tracing +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-opentelemetry = "0.25" +opentelemetry = "0.24" +opentelemetry_sdk = { version = "0.24", features = ["rt-tokio"] } +# default-features disabled to skip the grpc-tonic stack (tonic/tower/axum/h2); +# we only use the HTTP/protobuf exporter via reqwest. +opentelemetry-otlp = { version = "0.17", default-features = false, features = [ + "http-proto", + "reqwest-client", + "trace", +] } +tokio = { version = "1", features = ["rt-multi-thread"] } [profile.release] codegen-units = 1 diff --git a/rewatch/src/build.rs b/rewatch/src/build.rs index 88394eddda2..6254de3124f 100644 --- a/rewatch/src/build.rs +++ b/rewatch/src/build.rs @@ -28,6 +28,7 @@ use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; +use tracing::{info_span, instrument}; fn is_dirty(module: &Module) -> bool { match module.source_type { @@ -127,6 +128,7 @@ pub fn get_compiler_info(project_context: &ProjectContext) -> Result, filter: &Option, @@ -237,6 +239,7 @@ impl fmt::Display for IncrementalBuildError { } } +#[instrument(name = "incremental_build", skip_all, fields(module_count = build_state.modules.len()))] pub fn incremental_build( build_state: &mut BuildCommandState, default_timing: Option, @@ -279,6 +282,7 @@ pub fn incremental_build( warnings } Err(err) => { + let _error_span = info_span!("build.parse_error").entered(); logs::finalize(&build_state.packages); if !plain_output && show_progress { @@ -374,6 +378,7 @@ pub fn incremental_build( } pb.finish(); if !compile_errors.is_empty() { + let _error_span = info_span!("build.compile_error").entered(); if show_progress { if plain_output { eprintln!("Compiled {num_compiled_modules} modules") @@ -389,6 +394,7 @@ pub fn incremental_build( } } if helpers::contains_ascii_characters(&compile_warnings) { + let _warning_span = info_span!("build.compile_warning").entered(); eprintln!("{}", &compile_warnings); } if initial_build { diff --git a/rewatch/src/build/clean.rs b/rewatch/src/build/clean.rs index 73e77987dd7..40e38d4f7bd 100644 --- a/rewatch/src/build/clean.rs +++ b/rewatch/src/build/clean.rs @@ -13,6 +13,7 @@ use rayon::prelude::*; use std::io::Write; use std::path::{Path, PathBuf}; use std::time::Instant; +use tracing::instrument; fn remove_ast(package: &packages::Package, source_file: &Path) { let _ = std::fs::remove_file(helpers::get_compiler_asset( @@ -102,6 +103,7 @@ fn clean_source_files(build_state: &BuildState, root_config: &Config) { // TODO: change to scan_previous_build => CompileAssetsState // and then do cleanup on that state (for instance remove all .mjs files that are not in the state) +#[instrument(name = "clean.cleanup_previous_build", skip_all)] pub fn cleanup_previous_build( build_state: &mut BuildCommandState, compile_assets_state: CompileAssetsState, @@ -332,6 +334,7 @@ pub fn cleanup_after_build(build_state: &BuildCommandState) { }); } +#[instrument(name = "clean.clean", skip_all)] pub fn clean(path: &Path, show_progress: bool, plain_output: bool, prod: bool) -> Result<()> { let project_context = ProjectContext::new(path)?; let compiler_info = build::get_compiler_info(&project_context)?; diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index 5247342a2a3..e0bbf0730bf 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -20,6 +20,7 @@ use std::path::PathBuf; use std::process::Command; use std::sync::OnceLock; use std::time::SystemTime; +use tracing::{info_span, instrument}; /// Execute js-post-build command for a compiled JavaScript file. /// The command runs in the directory containing the rescript.json that defines it. @@ -27,6 +28,13 @@ use std::time::SystemTime; fn execute_post_build_command(cmd: &str, js_file_path: &Path, working_dir: &Path) -> Result<()> { let full_command = format!("{} {}", cmd, js_file_path.display()); + let _span = info_span!( + "build.js_post_build", + command = %cmd, + js_file = %js_file_path.display(), + ) + .entered(); + debug!( "Executing js-post-build: {} (in {})", full_command, @@ -72,6 +80,7 @@ fn execute_post_build_command(cmd: &str, js_file_path: &Path, working_dir: &Path } } +#[instrument(name = "build.compile", skip_all)] pub fn compile( build_state: &mut BuildCommandState, show_progress: bool, @@ -164,6 +173,21 @@ pub fn compile( let current_in_progres_modules = in_progress_modules.clone(); + // Count files that will be compiled in this wave + let wave_file_count = current_in_progres_modules + .iter() + .filter(|module_name| { + let module = build_state.get_module(module_name).unwrap(); + module + .deps + .intersection(&compile_universe) + .all(|dep| compiled_modules.contains(dep)) + && module.compile_dirty + }) + .count(); + + let _wave_span = info_span!("build.compile_wave", file_count = wave_file_count).entered(); + let results = current_in_progres_modules .par_iter() .filter_map(|module_name| { @@ -196,6 +220,9 @@ pub fn compile( )) } SourceType::SourceFile(source_file) => { + let _file_span = + info_span!("build.compile_file", module = %module_name).entered(); + let cmi_path = helpers::get_compiler_asset( package, &package.namespace, diff --git a/rewatch/src/build/packages.rs b/rewatch/src/build/packages.rs index a936b415f65..1754fa9d0f1 100644 --- a/rewatch/src/build/packages.rs +++ b/rewatch/src/build/packages.rs @@ -18,6 +18,7 @@ use std::fs::{self}; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use std::time::SystemTime; +use tracing::{info_span, instrument}; #[derive(Debug, Clone)] pub struct SourceFileMeta { @@ -607,6 +608,7 @@ fn extend_with_children( prod: bool, ) -> AHashMap { for (_key, package) in build.iter_mut() { + let _span = info_span!("build.load_package_sources", package = %package.name).entered(); let mut map: AHashMap = AHashMap::new(); package .source_folders @@ -656,6 +658,7 @@ fn extend_with_children( /// interface files. /// /// The two step process is there to reduce IO overhead. +#[instrument(name = "packages.make", skip_all)] pub fn make( filter: &Option, project_context: &ProjectContext, @@ -671,6 +674,7 @@ pub fn make( Ok(result) } +#[instrument(name = "packages.parse_packages", skip_all)] pub fn parse_packages(build_state: &mut BuildState) -> Result<()> { let packages = build_state.packages.clone(); for (package_name, package) in packages.iter() { diff --git a/rewatch/src/build/parse.rs b/rewatch/src/build/parse.rs index 223d49d90ea..46a4c866d80 100644 --- a/rewatch/src/build/parse.rs +++ b/rewatch/src/build/parse.rs @@ -12,6 +12,7 @@ use log::debug; use rayon::prelude::*; use std::path::{Path, PathBuf}; use std::process::Command; +use tracing::info_span; pub fn generate_asts( build_state: &mut BuildCommandState, @@ -20,6 +21,20 @@ pub fn generate_asts( let mut has_failure = false; let mut stderr = "".to_string(); + // Count dirty modules for the span + let dirty_modules = build_state + .modules + .values() + .filter(|m| match &m.source_type { + SourceType::SourceFile(sf) => { + sf.implementation.parse_dirty || sf.interface.as_ref().is_some_and(|i| i.parse_dirty) + } + SourceType::MlMap(mlmap) => mlmap.parse_dirty, + }) + .count(); + + let _span = info_span!("build.parse", dirty_modules = dirty_modules).entered(); + build_state .modules .par_iter() diff --git a/rewatch/src/format.rs b/rewatch/src/format.rs index c4ea876e451..1b318d0dad3 100644 --- a/rewatch/src/format.rs +++ b/rewatch/src/format.rs @@ -7,11 +7,13 @@ use std::io::{self, Write}; use std::path::Path; use std::process::Command; use std::sync::atomic::{AtomicUsize, Ordering}; +use tracing::{info_span, instrument}; use crate::build::packages; use crate::cli::FileExtension; use clap::ValueEnum; +#[instrument(name = "format.format", skip_all)] pub fn format(stdin_extension: Option, check: bool, files: Vec) -> Result<()> { let bsc_path = helpers::get_bsc(); @@ -102,6 +104,7 @@ fn format_files(bsc_exe: &Path, files: Vec, check: bool) -> Result<()> { eprintln!("[format check] {file}"); incorrectly_formatted_files.fetch_add(1, Ordering::SeqCst); } else { + let _file_span = info_span!("format.write_file", file = %file).entered(); // Only write if content actually changed fs::write(file, &*formatted_content)?; } diff --git a/rewatch/src/lib.rs b/rewatch/src/lib.rs index a389e8172ef..2216c16fa81 100644 --- a/rewatch/src/lib.rs +++ b/rewatch/src/lib.rs @@ -8,4 +8,5 @@ pub mod lock; pub mod project_context; pub mod queue; pub mod sourcedirs; +pub mod telemetry; pub mod watcher; diff --git a/rewatch/src/main.rs b/rewatch/src/main.rs index 9d3a2354355..7c922687936 100644 --- a/rewatch/src/main.rs +++ b/rewatch/src/main.rs @@ -1,37 +1,52 @@ -use anyhow::Result; use console::Term; use log::LevelFilter; use std::{io::Write, path::Path}; +use tracing::instrument; use rescript::{ build, cli, cmd, format, lock::{LockKind, drop_lock, get_lock_or_exit}, - watcher, + telemetry, watcher, }; -fn main() -> Result<()> { +fn main() { + // Initialize telemetry (only active if OTEL_EXPORTER_OTLP_ENDPOINT is set) + let telemetry_guard = telemetry::init_telemetry(); + + let exit_code = run_main(&telemetry_guard); + + // Drop the telemetry guard explicitly to ensure spans are flushed before exit + drop(telemetry_guard); + + std::process::exit(exit_code); +} + +fn run_main(telemetry_guard: &telemetry::TelemetryGuard) -> i32 { let cli = cli::parse_with_default().unwrap_or_else(|err| err.exit()); let log_level_filter = cli.verbose.log_level_filter(); - let stdout_logger = env_logger::Builder::new() - .format(|buf, record| writeln!(buf, "{}:\n{}", record.level(), record.args())) - .filter_level(log_level_filter) - .target(env_logger::fmt::Target::Stdout) - .build(); - - let stderr_logger = env_logger::Builder::new() - .format(|buf, record| writeln!(buf, "{}:\n{}", record.level(), record.args())) - .filter_level(log_level_filter) - .target(env_logger::fmt::Target::Stderr) - .build(); - - log::set_max_level(log_level_filter); - log::set_boxed_logger(Box::new(SplitLogger { - stdout: stdout_logger, - stderr: stderr_logger, - })) - .expect("Failed to initialize logger"); + // Only set up custom logger if OTEL is not enabled (OTEL sets up its own subscriber) + if !telemetry_guard.otel_enabled { + let stdout_logger = env_logger::Builder::new() + .format(|buf, record| writeln!(buf, "{}:\n{}", record.level(), record.args())) + .filter_level(log_level_filter) + .target(env_logger::fmt::Target::Stdout) + .build(); + + let stderr_logger = env_logger::Builder::new() + .format(|buf, record| writeln!(buf, "{}:\n{}", record.level(), record.args())) + .filter_level(log_level_filter) + .target(env_logger::fmt::Target::Stderr) + .build(); + + log::set_max_level(log_level_filter); + log::set_boxed_logger(Box::new(SplitLogger { + stdout: stdout_logger, + stderr: stderr_logger, + })) + .expect("Failed to initialize logger"); + } let is_tty: bool = Term::stdout().is_term() && Term::stderr().is_term(); let plain_output = !is_tty; @@ -41,61 +56,100 @@ fn main() -> Result<()> { let show_progress = log_level_filter == LevelFilter::Info; match cli.command { - cli::Command::CompilerArgs { path } => { - println!("{}", build::get_compiler_args(Path::new(&path))?); - std::process::exit(0); - } - cli::Command::Build(build_args) => { - match build::build( - &build_args.filter, - Path::new(&build_args.folder as &str), - show_progress, - build_args.no_timing, - true, // create_sourcedirs is now always enabled - plain_output, - (*build_args.warn_error).clone(), - build_args.prod, - ) { - Err(e) => { - eprintln!("{:#}", e); - std::process::exit(1) - } - Ok(_) => { - if let Some(args_after_build) = (*build_args.after_build).clone() { - cmd::run(args_after_build) - } - std::process::exit(0) - } - }; - } + cli::Command::CompilerArgs { path } => run_compiler_args(&path), + cli::Command::Build(build_args) => run_build(build_args, show_progress, plain_output), cli::Command::Watch(watch_args) => { let _lock = get_lock_or_exit(LockKind::Watch, &watch_args.folder); - - match watcher::start( - &watch_args.filter, - show_progress, - &watch_args.folder, - (*watch_args.after_build).clone(), - true, // create_sourcedirs is now always enabled - plain_output, - (*watch_args.warn_error).clone(), - watch_args.prod, - ) { - Err(e) => { - eprintln!("{:#}", e); - std::process::exit(1) - } - Ok(_) => Ok(()), - } + run_watch(watch_args, show_progress, plain_output) } cli::Command::Clean { folder, prod } => { let _lock = get_lock_or_exit(LockKind::Build, &folder); - let result = build::clean::clean(Path::new(&folder as &str), show_progress, plain_output, prod); - let _lock = drop_lock(LockKind::Build, &folder); + let code = run_clean(&folder, show_progress, plain_output, prod); + let _ = drop_lock(LockKind::Build, &folder); + code + } + cli::Command::Format { stdin, check, files } => run_format(stdin, check, files), + } +} + +#[instrument(name = "rewatch.compiler_args", skip_all, fields(file_path = %path))] +fn run_compiler_args(path: &str) -> i32 { + match build::get_compiler_args(Path::new(path)) { + Ok(args) => { + println!("{}", args); + 0 + } + Err(e) => { + eprintln!("{:#}", e); + 1 + } + } +} + +#[instrument(name = "rewatch.build", skip_all, fields(working_dir = %build_args.folder.folder))] +fn run_build(build_args: cli::BuildArgs, show_progress: bool, plain_output: bool) -> i32 { + match build::build( + &build_args.filter, + Path::new(&build_args.folder as &str), + show_progress, + build_args.no_timing, + true, // create_sourcedirs is now always enabled + plain_output, + (*build_args.warn_error).clone(), + build_args.prod, + ) { + Err(e) => { + eprintln!("{:#}", e); + 1 + } + Ok(_) => { + if let Some(args_after_build) = (*build_args.after_build).clone() { + cmd::run(args_after_build) + } + 0 + } + } +} + +#[instrument(name = "rewatch.watch", skip_all, fields(working_dir = %watch_args.folder.folder))] +fn run_watch(watch_args: cli::WatchArgs, show_progress: bool, plain_output: bool) -> i32 { + match watcher::start( + &watch_args.filter, + show_progress, + &watch_args.folder, + (*watch_args.after_build).clone(), + true, // create_sourcedirs is now always enabled + plain_output, + (*watch_args.warn_error).clone(), + watch_args.prod, + ) { + Err(e) => { + eprintln!("{:#}", e); + 1 + } + Ok(_) => 0, + } +} + +#[instrument(name = "rewatch.clean", skip_all, fields(working_dir = %folder))] +fn run_clean(folder: &str, show_progress: bool, plain_output: bool, prod: bool) -> i32 { + match build::clean::clean(Path::new(folder), show_progress, plain_output, prod) { + Ok(_) => 0, + Err(e) => { + eprintln!("{:#}", e); + 1 + } + } +} - result +#[instrument(name = "rewatch.format", skip_all, fields(check = check, is_stdin = stdin.is_some()))] +fn run_format(stdin: Option, check: bool, files: Vec) -> i32 { + match format::format(stdin, check, files) { + Ok(_) => 0, + Err(e) => { + eprintln!("{:#}", e); + 1 } - cli::Command::Format { stdin, check, files } => format::format(stdin, check, files), } } diff --git a/rewatch/src/telemetry.rs b/rewatch/src/telemetry.rs new file mode 100644 index 00000000000..508a424d499 --- /dev/null +++ b/rewatch/src/telemetry.rs @@ -0,0 +1,141 @@ +//! OpenTelemetry setup for rewatch. +//! +//! Provides optional tracing export via OTLP HTTP when OTEL_EXPORTER_OTLP_ENDPOINT is set. +//! When the environment variable is not set, tracing is disabled (no-op). + +use opentelemetry::KeyValue; +use opentelemetry::trace::TracerProvider as _; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::Resource; +use opentelemetry_sdk::trace::TracerProvider; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +/// Guard that ensures telemetry is properly flushed and shut down. +/// Drop this at the end of main to flush all spans. +pub struct TelemetryGuard { + provider: Option, + // Keep the runtime alive for the batch exporter + _runtime: Option, + /// Whether OTEL tracing was initialized (affects logger setup in main) + pub otel_enabled: bool, +} + +impl Drop for TelemetryGuard { + fn drop(&mut self) { + if let Some(provider) = self.provider.take() { + // Force flush - this exports any buffered spans + for result in provider.force_flush() { + if let Err(e) = result { + log::warn!("Error flushing tracer provider: {}", e); + } + } + // Shutdown + if let Err(e) = provider.shutdown() { + log::warn!("Error shutting down tracer provider: {}", e); + } + } + // Runtime is dropped after provider is shut down + } +} + +/// Initialize OpenTelemetry tracing if OTEL_EXPORTER_OTLP_ENDPOINT is set. +/// +/// Returns a guard that must be kept alive for the duration of the program. +/// When the guard is dropped, it flushes all pending spans. +/// +/// If OTEL_EXPORTER_OTLP_ENDPOINT is not set, returns a no-op guard and +/// tracing calls become no-ops. +pub fn init_telemetry() -> TelemetryGuard { + let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok(); + + match endpoint { + Some(endpoint) => init_with_otlp(&endpoint), + None => init_noop(), + } +} + +/// Initialize with OTLP exporter. +fn init_with_otlp(endpoint: &str) -> TelemetryGuard { + use opentelemetry_sdk::trace::BatchConfigBuilder; + use std::time::Duration; + + // Create a Tokio runtime for the batch exporter + let runtime = match tokio::runtime::Builder::new_multi_thread().enable_all().build() { + Ok(rt) => rt, + Err(e) => { + log::warn!("Failed to create Tokio runtime: {}. Falling back to no-op.", e); + return init_noop(); + } + }; + + // Enter the runtime context for creating the provider + let _guard = runtime.enter(); + + // Create resource to identify this service + let resource = Resource::new(vec![KeyValue::new("service.name", "rewatch")]); + + // Configure trace with resource + #[allow(deprecated)] + let trace_config = opentelemetry_sdk::trace::Config::default().with_resource(resource); + + // Configure batch exporter with shorter interval for faster export during tests + let batch_config = BatchConfigBuilder::default() + .with_scheduled_delay(Duration::from_millis(100)) + .build(); + + // Use HTTP exporter - endpoint should include /v1/traces path + let traces_endpoint = format!("{}/v1/traces", endpoint.trim_end_matches('/')); + + // Create tracer provider using HTTP OTLP exporter + let provider = match opentelemetry_otlp::new_pipeline() + .tracing() + .with_exporter( + opentelemetry_otlp::new_exporter() + .http() + .with_endpoint(&traces_endpoint), + ) + .with_trace_config(trace_config) + .with_batch_config(batch_config) + .install_batch(opentelemetry_sdk::runtime::Tokio) + { + Ok(provider) => provider, + Err(e) => { + log::warn!("Failed to create OTLP tracer: {}. Falling back to no-op.", e); + return init_noop(); + } + }; + + // Get a tracer from the provider + let tracer = provider.tracer("rewatch"); + + // Create tracing-opentelemetry layer + let telemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer); + + // Set up tracing subscriber with OpenTelemetry layer. + // Default to debug level so that diagnostic tracing::debug! events + // (e.g. in the watcher) are captured as OTEL span events. + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")); + + tracing_subscriber::registry() + .with(filter) + .with(telemetry_layer) + .init(); + + TelemetryGuard { + provider: Some(provider), + _runtime: Some(runtime), + otel_enabled: true, + } +} + +/// Initialize with no-op tracing (no export). +fn init_noop() -> TelemetryGuard { + // Don't set up any subscriber - let main.rs handle logging normally + TelemetryGuard { + provider: None, + _runtime: None, + otel_enabled: false, + } +} diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index d6aba426f92..dd904913f0b 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -267,6 +267,12 @@ async fn async_watch( ) { log::debug!("rescript.json changed -> full compile"); + tracing::debug!( + reason = "rescript.json changed", + event_kind = ?event.kind, + paths = ?event.paths, + "watcher.full_compile_triggered" + ); needs_compile_type = CompileType::Full; continue; } From 2fcad19f6992f1eba44ca1acbf0be5d5837d9b9d Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 14:08:54 +0200 Subject: [PATCH 02/19] Improve rewatch span propagation Propagate parent span through rayon in build.parse so build.parse_file spans are properly nested under build.parse instead of appearing as orphaned root spans. Enrich build.compile_file span with package, suffix, module_system, and namespace attributes for better observability. Cherry-picked from #8241. --- rewatch/src/build/compile.rs | 7 +++++- rewatch/src/build/parse.rs | 48 +++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index e0bbf0730bf..5a28daf75c8 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -220,8 +220,13 @@ pub fn compile( )) } SourceType::SourceFile(source_file) => { + let root_config = build_state.get_root_config(); + let first_spec = root_config.get_package_specs().into_iter().next(); + let suffix = first_spec.as_ref().map(|s| root_config.get_suffix(s)).unwrap_or_default(); + let module_system = first_spec.as_ref().map(|s| s.module.as_str()).unwrap_or("esmodule"); + let namespace = package.namespace.to_suffix().unwrap_or_default(); let _file_span = - info_span!("build.compile_file", module = %module_name).entered(); + info_span!("build.compile_file", module = %module_name, package = %package.name, suffix, module_system, namespace).entered(); let cmi_path = helpers::get_compiler_asset( package, diff --git a/rewatch/src/build/parse.rs b/rewatch/src/build/parse.rs index 46a4c866d80..e963810c2b4 100644 --- a/rewatch/src/build/parse.rs +++ b/rewatch/src/build/parse.rs @@ -33,7 +33,8 @@ pub fn generate_asts( }) .count(); - let _span = info_span!("build.parse", dirty_modules = dirty_modules).entered(); + let parse_span = info_span!("build.parse", dirty_modules = dirty_modules); + let _span = parse_span.enter(); build_state .modules @@ -68,6 +69,7 @@ pub fn generate_asts( &source_file.implementation.path.to_owned(), build_state, build_state.get_warn_error_override(), + &parse_span, ) .map_err(|e| e.to_string()); @@ -78,6 +80,7 @@ pub fn generate_asts( &interface_file_path.to_owned(), build_state, build_state.get_warn_error_override(), + &parse_span, ) { Ok(v) => Ok(Some(v)), Err(e) => Err(e.to_string()), @@ -337,11 +340,48 @@ pub fn parser_args( )) } +fn make_parse_file_span( + filename: &Path, + package: &Package, + parser_args: &[String], + build_state: &BuildState, + parent: &tracing::Span, +) -> tracing::Span { + let module_name = helpers::file_path_to_module_name(filename, &package.namespace); + let mut ppx_names: Vec = Vec::new(); + let mut experimental: Vec = Vec::new(); + for pair in parser_args.windows(2) { + if pair[0] == "-ppx" { + let parts: Vec<&str> = pair[1].split(['/', '\\']).collect(); + let start = if parts.len() >= 2 { parts.len() - 2 } else { 0 }; + ppx_names.push(parts[start..].join("/")); + } else if pair[0] == "-enable-experimental" { + experimental.push(pair[1].clone()); + } + } + let ppx = ppx_names.join(","); + let experimental = experimental.join(","); + let root_config = build_state.get_root_config(); + let jsx = root_config.get_jsx_args().get(1).cloned().unwrap_or_default(); + let bsc_flags = config::flatten_flags(&package.config.compiler_flags).join(" "); + info_span!( + parent: parent, + "build.parse_file", + module = %module_name, + package = %package.name, + ppx = %ppx, + experimental = %experimental, + jsx = %jsx, + bsc_flags = %bsc_flags, + ) +} + fn generate_ast( package: Package, filename: &Path, build_state: &BuildState, warn_error_override: Option, + parent_span: &tracing::Span, ) -> anyhow::Result<(PathBuf, Option)> { let file_path = PathBuf::from(&package.path).join(filename); let contents = helpers::read_file(&file_path).expect("Error reading file"); @@ -356,6 +396,12 @@ fn generate_ast( warn_error_override, )?; + let _parse_span = if tracing::enabled!(tracing::Level::INFO) { + make_parse_file_span(filename, &package, &parser_args, build_state, parent_span).entered() + } else { + tracing::Span::none().entered() + }; + // generate the dir of the ast_path (it mirrors the source file dir) let ast_parent_path = package.get_build_path().join(ast_path.parent().unwrap()); helpers::create_path(&ast_parent_path); From d2a7396f65a89f1482a4e9a0dbe21a5d1b4b1c70 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 14:09:24 +0200 Subject: [PATCH 03/19] Fix compile_file spans not nested under compile_wave in OTEL traces Tracing spans are thread-local, so compile_file spans created inside Rayon's par_iter had no parent connection to the compile_wave span on the main thread. Pass the wave span explicitly via `parent: &wave_span` to establish the correct parent-child relationship. Cherry-picked from #8241. --- AGENTS.md | 20 ++++++++++++++++++++ rewatch/src/build/compile.rs | 24 ++++++++---------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 465ebfc4d39..9dcf7481731 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -410,6 +410,26 @@ This is useful for iterating on a specific test without running the full suite. - **Dependencies**: Inspect module dependency graph in `deps.rs` - **File Watching**: Monitor file change events in `watcher.rs` +#### OpenTelemetry Tracing + +Rewatch supports OpenTelemetry (OTEL) tracing for build and watch commands. To visualize traces locally, run a Jaeger all-in-one container: + +```bash +docker run -d --name jaeger \ + -p 4317:4317 -p 4318:4318 -p 16686:16686 \ + jaegertracing/all-in-one +``` + +Then run rewatch with the OTLP endpoint set: + +```bash +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 cargo run --manifest-path rewatch/Cargo.toml -- build +``` + +Open http://localhost:16686 to view traces in the Jaeger UI. + +Note: Use `tracing::debug!` (not `log::debug!`) for events you want to appear in OTEL traces — they use separate logging systems. + #### Running Rewatch Directly When running the rewatch binary directly (via `cargo run` or the compiled binary) during development, you need to set environment variables to point to the local compiler and runtime. Otherwise, rewatch will try to use the installed versions: diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index 5a28daf75c8..db4e8abaa78 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -171,22 +171,14 @@ pub fn compile( loop_count, ); - let current_in_progres_modules = in_progress_modules.clone(); - - // Count files that will be compiled in this wave - let wave_file_count = current_in_progres_modules - .iter() - .filter(|module_name| { - let module = build_state.get_module(module_name).unwrap(); - module - .deps - .intersection(&compile_universe) - .all(|dep| compiled_modules.contains(dep)) - && module.compile_dirty - }) - .count(); + let wave_span = info_span!( + "build.compile_wave", + wave = loop_count, + file_count = in_progress_modules.len(), + ); + let _wave_entered = wave_span.enter(); - let _wave_span = info_span!("build.compile_wave", file_count = wave_file_count).entered(); + let current_in_progres_modules = in_progress_modules.clone(); let results = current_in_progres_modules .par_iter() @@ -226,7 +218,7 @@ pub fn compile( let module_system = first_spec.as_ref().map(|s| s.module.as_str()).unwrap_or("esmodule"); let namespace = package.namespace.to_suffix().unwrap_or_default(); let _file_span = - info_span!("build.compile_file", module = %module_name, package = %package.name, suffix, module_system, namespace).entered(); + info_span!(parent: &wave_span, "build.compile_file", module = %module_name, package = %package.name, suffix, module_system, namespace).entered(); let cmi_path = helpers::get_compiler_asset( package, From ece9e1b9f8209b6b9e0d44ec194dafe24b155395 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 14:09:43 +0200 Subject: [PATCH 04/19] Add changelog entry for OpenTelemetry support --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 314e08424e1..863bd868efb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ #### :nail_care: Polish - Allow builds while watchers are running. https://github.com/rescript-lang/rescript/pull/8349 +- Build system: Add OpenTelemetry tracing support for cli commands. #### :house: Internal From 55993c2c6c9a9465d477d1480705fd36ebe60220 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:00:37 +0200 Subject: [PATCH 05/19] Rewatch: don't suppress logs when OTEL telemetry is enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OTEL layer only exports spans — it does not print events. Without a fmt layer alongside it, `log::warn!` / `tracing::error!` and similar output was silently swallowed whenever OTEL_EXPORTER_OTLP_ENDPOINT was set, leaving users with no visible output at all. Add a fmt layer writing to stderr so logs continue to surface when telemetry is on. --- rewatch/src/telemetry.rs | 88 +++++++++++++++------------------------- 1 file changed, 32 insertions(+), 56 deletions(-) diff --git a/rewatch/src/telemetry.rs b/rewatch/src/telemetry.rs index 508a424d499..c50720a1675 100644 --- a/rewatch/src/telemetry.rs +++ b/rewatch/src/telemetry.rs @@ -5,19 +5,18 @@ use opentelemetry::KeyValue; use opentelemetry::trace::TracerProvider as _; -use opentelemetry_otlp::WithExportConfig; +use opentelemetry_otlp::{Protocol, SpanExporter, WithExportConfig}; use opentelemetry_sdk::Resource; -use opentelemetry_sdk::trace::TracerProvider; +use opentelemetry_sdk::trace::SdkTracerProvider; use tracing_subscriber::EnvFilter; +use tracing_subscriber::fmt; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; /// Guard that ensures telemetry is properly flushed and shut down. /// Drop this at the end of main to flush all spans. pub struct TelemetryGuard { - provider: Option, - // Keep the runtime alive for the batch exporter - _runtime: Option, + provider: Option, /// Whether OTEL tracing was initialized (affects logger setup in main) pub otel_enabled: bool, } @@ -26,17 +25,14 @@ impl Drop for TelemetryGuard { fn drop(&mut self) { if let Some(provider) = self.provider.take() { // Force flush - this exports any buffered spans - for result in provider.force_flush() { - if let Err(e) = result { - log::warn!("Error flushing tracer provider: {}", e); - } + if let Err(e) = provider.force_flush() { + log::warn!("Error flushing tracer provider: {}", e); } // Shutdown if let Err(e) = provider.shutdown() { log::warn!("Error shutting down tracer provider: {}", e); } } - // Runtime is dropped after provider is shut down } } @@ -58,74 +54,55 @@ pub fn init_telemetry() -> TelemetryGuard { /// Initialize with OTLP exporter. fn init_with_otlp(endpoint: &str) -> TelemetryGuard { - use opentelemetry_sdk::trace::BatchConfigBuilder; - use std::time::Duration; - - // Create a Tokio runtime for the batch exporter - let runtime = match tokio::runtime::Builder::new_multi_thread().enable_all().build() { - Ok(rt) => rt, - Err(e) => { - log::warn!("Failed to create Tokio runtime: {}. Falling back to no-op.", e); - return init_noop(); - } - }; - - // Enter the runtime context for creating the provider - let _guard = runtime.enter(); - - // Create resource to identify this service - let resource = Resource::new(vec![KeyValue::new("service.name", "rewatch")]); - - // Configure trace with resource - #[allow(deprecated)] - let trace_config = opentelemetry_sdk::trace::Config::default().with_resource(resource); - - // Configure batch exporter with shorter interval for faster export during tests - let batch_config = BatchConfigBuilder::default() - .with_scheduled_delay(Duration::from_millis(100)) - .build(); - // Use HTTP exporter - endpoint should include /v1/traces path let traces_endpoint = format!("{}/v1/traces", endpoint.trim_end_matches('/')); - // Create tracer provider using HTTP OTLP exporter - let provider = match opentelemetry_otlp::new_pipeline() - .tracing() - .with_exporter( - opentelemetry_otlp::new_exporter() - .http() - .with_endpoint(&traces_endpoint), - ) - .with_trace_config(trace_config) - .with_batch_config(batch_config) - .install_batch(opentelemetry_sdk::runtime::Tokio) + // Build a blocking reqwest-backed HTTP exporter. Blocking is fine here + // because the batch span processor runs in its own background thread. + let exporter = match SpanExporter::builder() + .with_http() + .with_endpoint(&traces_endpoint) + .with_protocol(Protocol::HttpBinary) + .build() { - Ok(provider) => provider, + Ok(exp) => exp, Err(e) => { - log::warn!("Failed to create OTLP tracer: {}. Falling back to no-op.", e); + log::warn!("Failed to create OTLP exporter: {}. Falling back to no-op.", e); return init_noop(); } }; - // Get a tracer from the provider - let tracer = provider.tracer("rewatch"); + // Identify this service in exported spans. + let resource = Resource::builder() + .with_attribute(KeyValue::new("service.name", "rewatch")) + .build(); - // Create tracing-opentelemetry layer + let provider = SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .with_resource(resource) + .build(); + + // Bridge the `tracing` crate into this provider. + let tracer = provider.tracer("rewatch"); let telemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer); - // Set up tracing subscriber with OpenTelemetry layer. // Default to debug level so that diagnostic tracing::debug! events // (e.g. in the watcher) are captured as OTEL span events. let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")); + // Also write logs to stderr. Without this layer, `log::*` / `tracing::*` + // output is silently swallowed when telemetry is enabled, since the + // OTEL layer only exports spans — it does not print them. + let fmt_layer = fmt::layer().with_writer(std::io::stderr); + tracing_subscriber::registry() .with(filter) .with(telemetry_layer) + .with(fmt_layer) .init(); TelemetryGuard { provider: Some(provider), - _runtime: Some(runtime), otel_enabled: true, } } @@ -135,7 +112,6 @@ fn init_noop() -> TelemetryGuard { // Don't set up any subscriber - let main.rs handle logging normally TelemetryGuard { provider: None, - _runtime: None, otel_enabled: false, } } From 4b2c86dfce3f0f34671194865b40eeea4f56c0bf Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:01:33 +0200 Subject: [PATCH 06/19] Rewatch: gate compile_file span attrs behind tracing::enabled! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-module build.compile_file span eagerly fetched root_config, cloned the package-spec list, and formatted Display values on every iteration — even when no subscriber was listening. For projects with hundreds of modules this is meaningful wasted work in the hot path. Wrap the span + attribute construction in tracing::enabled!(INFO) and fall back to Span::none() otherwise, matching the pattern already used in build/parse.rs. --- rewatch/src/build/compile.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index db4e8abaa78..849066b99b5 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -212,13 +212,19 @@ pub fn compile( )) } SourceType::SourceFile(source_file) => { - let root_config = build_state.get_root_config(); - let first_spec = root_config.get_package_specs().into_iter().next(); - let suffix = first_spec.as_ref().map(|s| root_config.get_suffix(s)).unwrap_or_default(); - let module_system = first_spec.as_ref().map(|s| s.module.as_str()).unwrap_or("esmodule"); - let namespace = package.namespace.to_suffix().unwrap_or_default(); - let _file_span = - info_span!(parent: &wave_span, "build.compile_file", module = %module_name, package = %package.name, suffix, module_system, namespace).entered(); + // Construct span + attributes only when a subscriber is listening. + // Otherwise root_config.get_package_specs() (which clones) and the + // attribute formatting run per-module on the build hot path. + let _file_span = if tracing::enabled!(tracing::Level::INFO) { + let root_config = build_state.get_root_config(); + let first_spec = root_config.get_package_specs().into_iter().next(); + let suffix = first_spec.as_ref().map(|s| root_config.get_suffix(s)).unwrap_or_default(); + let module_system = first_spec.as_ref().map(|s| s.module.as_str()).unwrap_or("esmodule"); + let namespace = package.namespace.to_suffix().unwrap_or_default(); + info_span!(parent: &wave_span, "build.compile_file", module = %module_name, package = %package.name, suffix, module_system, namespace).entered() + } else { + tracing::Span::none().entered() + }; let cmi_path = helpers::get_compiler_asset( package, From efb8486f27418619d8ee60c1047a9a7201e3b0d0 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:02:15 +0200 Subject: [PATCH 07/19] Rewatch: report all package-spec suffixes/module_systems on compile_file span When a package is built under multiple specs (e.g. ESM + CJS), the compile_file span previously only reflected the first spec and silently defaulted `module_system` to "esmodule" if none matched. That lost information in exactly the configurations where telemetry would be most useful. Join all specs with commas so the span reports the full set of targets the compilation step actually covers. --- rewatch/src/build/compile.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index 849066b99b5..2a7ceb849cb 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -217,9 +217,20 @@ pub fn compile( // attribute formatting run per-module on the build hot path. let _file_span = if tracing::enabled!(tracing::Level::INFO) { let root_config = build_state.get_root_config(); - let first_spec = root_config.get_package_specs().into_iter().next(); - let suffix = first_spec.as_ref().map(|s| root_config.get_suffix(s)).unwrap_or_default(); - let module_system = first_spec.as_ref().map(|s| s.module.as_str()).unwrap_or("esmodule"); + let specs = root_config.get_package_specs(); + // A package can be built under multiple specs (e.g. ESM + CJS). + // Report all of them joined by "," instead of silently picking + // the first — the compile_file span covers work for every spec. + let suffix = specs + .iter() + .map(|s| root_config.get_suffix(s)) + .collect::>() + .join(","); + let module_system = specs + .iter() + .map(|s| s.module.as_str()) + .collect::>() + .join(","); let namespace = package.namespace.to_suffix().unwrap_or_default(); info_span!(parent: &wave_span, "build.compile_file", module = %module_name, package = %package.name, suffix, module_system, namespace).entered() } else { From 937046bc2577c9a62f4c50cd54ff486cb8c2eaf0 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:03:05 +0200 Subject: [PATCH 08/19] Rewatch telemetry: honor OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES Previously `service.name` was hardcoded to "rewatch", overriding whatever the user supplied via OTEL_SERVICE_NAME. Resource::builder() already runs the env-var resource detectors, so default to "rewatch" only when the env var is unset. This also picks up OTEL_RESOURCE_ATTRIBUTES for free. --- rewatch/src/telemetry.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/rewatch/src/telemetry.rs b/rewatch/src/telemetry.rs index c50720a1675..c342c90e4cb 100644 --- a/rewatch/src/telemetry.rs +++ b/rewatch/src/telemetry.rs @@ -3,7 +3,6 @@ //! Provides optional tracing export via OTLP HTTP when OTEL_EXPORTER_OTLP_ENDPOINT is set. //! When the environment variable is not set, tracing is disabled (no-op). -use opentelemetry::KeyValue; use opentelemetry::trace::TracerProvider as _; use opentelemetry_otlp::{Protocol, SpanExporter, WithExportConfig}; use opentelemetry_sdk::Resource; @@ -72,10 +71,15 @@ fn init_with_otlp(endpoint: &str) -> TelemetryGuard { } }; - // Identify this service in exported spans. - let resource = Resource::builder() - .with_attribute(KeyValue::new("service.name", "rewatch")) - .build(); + // Resource::builder() applies the SdkProvidedResourceDetector and + // EnvResourceDetector, which already pick up OTEL_SERVICE_NAME and + // OTEL_RESOURCE_ATTRIBUTES from the environment. Only default + // service.name = "rewatch" when the user hasn't supplied their own. + let mut resource_builder = Resource::builder(); + if std::env::var_os("OTEL_SERVICE_NAME").is_none() { + resource_builder = resource_builder.with_service_name("rewatch"); + } + let resource = resource_builder.build(); let provider = SdkTracerProvider::builder() .with_batch_exporter(exporter) From 4b972a07ad27e02db6b83146db8877f12bf444c6 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:04:19 +0200 Subject: [PATCH 09/19] Rewatch telemetry: let SDK resolve the OTLP endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously we appended "/v1/traces" manually and passed the result to .with_endpoint(), which bypassed the SDK's own endpoint resolution and overrode any value the user set via OTEL_EXPORTER_OTLP_TRACES_ENDPOINT. Drop the manual path munging: opentelemetry-otlp reads the trace-specific env var verbatim, falls back to OTEL_EXPORTER_OTLP_ENDPOINT (appending /v1/traces), or the default — matching the OTEL spec. Also enable telemetry when only the trace-specific env var is set. --- rewatch/src/telemetry.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/rewatch/src/telemetry.rs b/rewatch/src/telemetry.rs index c342c90e4cb..34859b6c7a4 100644 --- a/rewatch/src/telemetry.rs +++ b/rewatch/src/telemetry.rs @@ -43,24 +43,23 @@ impl Drop for TelemetryGuard { /// If OTEL_EXPORTER_OTLP_ENDPOINT is not set, returns a no-op guard and /// tracing calls become no-ops. pub fn init_telemetry() -> TelemetryGuard { - let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok(); + // Presence of either the general or trace-specific endpoint env var enables OTLP export. + let enabled = std::env::var_os("OTEL_EXPORTER_OTLP_ENDPOINT").is_some() + || std::env::var_os("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT").is_some(); - match endpoint { - Some(endpoint) => init_with_otlp(&endpoint), - None => init_noop(), - } + if enabled { init_with_otlp() } else { init_noop() } } /// Initialize with OTLP exporter. -fn init_with_otlp(endpoint: &str) -> TelemetryGuard { - // Use HTTP exporter - endpoint should include /v1/traces path - let traces_endpoint = format!("{}/v1/traces", endpoint.trim_end_matches('/')); - +fn init_with_otlp() -> TelemetryGuard { // Build a blocking reqwest-backed HTTP exporter. Blocking is fine here // because the batch span processor runs in its own background thread. + // + // The exporter reads OTEL_EXPORTER_OTLP_TRACES_ENDPOINT (used verbatim) + // or falls back to OTEL_EXPORTER_OTLP_ENDPOINT (with /v1/traces appended), + // matching the OTEL spec — so we pass no explicit endpoint here. let exporter = match SpanExporter::builder() .with_http() - .with_endpoint(&traces_endpoint) .with_protocol(Protocol::HttpBinary) .build() { From 6ea8c657ed0b68c0a97047bbc36ed19fa8caf638 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:04:47 +0200 Subject: [PATCH 10/19] Rewatch: replace TelemetryGuard ref with a plain bool in run_main run_main only reads .otel_enabled once; the reference parameter added noise without benefit. Pass a bool instead. --- rewatch/src/main.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rewatch/src/main.rs b/rewatch/src/main.rs index 7c922687936..be532104ad0 100644 --- a/rewatch/src/main.rs +++ b/rewatch/src/main.rs @@ -12,8 +12,9 @@ use rescript::{ fn main() { // Initialize telemetry (only active if OTEL_EXPORTER_OTLP_ENDPOINT is set) let telemetry_guard = telemetry::init_telemetry(); + let otel_enabled = telemetry_guard.otel_enabled; - let exit_code = run_main(&telemetry_guard); + let exit_code = run_main(otel_enabled); // Drop the telemetry guard explicitly to ensure spans are flushed before exit drop(telemetry_guard); @@ -21,13 +22,13 @@ fn main() { std::process::exit(exit_code); } -fn run_main(telemetry_guard: &telemetry::TelemetryGuard) -> i32 { +fn run_main(otel_enabled: bool) -> i32 { let cli = cli::parse_with_default().unwrap_or_else(|err| err.exit()); let log_level_filter = cli.verbose.log_level_filter(); // Only set up custom logger if OTEL is not enabled (OTEL sets up its own subscriber) - if !telemetry_guard.otel_enabled { + if !otel_enabled { let stdout_logger = env_logger::Builder::new() .format(|buf, record| writeln!(buf, "{}:\n{}", record.level(), record.args())) .filter_level(log_level_filter) From 34794c5c3e6195be466c7a21befe64624163c286 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:05:04 +0200 Subject: [PATCH 11/19] Rewatch: explain why the telemetry guard must be dropped before exit std::process::exit skips Drop, so without the explicit drop the guard's flush would never run and buffered OTLP spans would be lost. Add a comment so the requirement isn't lost in a future cleanup. --- rewatch/src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rewatch/src/main.rs b/rewatch/src/main.rs index be532104ad0..2c089627b41 100644 --- a/rewatch/src/main.rs +++ b/rewatch/src/main.rs @@ -16,7 +16,9 @@ fn main() { let exit_code = run_main(otel_enabled); - // Drop the telemetry guard explicitly to ensure spans are flushed before exit + // std::process::exit terminates the process without running Drops for + // values still on the stack, so the TelemetryGuard's Drop (which flushes + // buffered OTLP spans) would be skipped. Drop explicitly first. drop(telemetry_guard); std::process::exit(exit_code); From 7dc20ee10c19077e98ad248527b85d2c32c33288 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:05:44 +0200 Subject: [PATCH 12/19] Rewatch telemetry: standardize span names to dotted style Rename the two outlier snake_case span names to the dotted convention used everywhere else: initialize_build -> build.initialize incremental_build -> build.incremental Consistent dotted names make Jaeger / OTEL UI filtering predictable (e.g. `name ^= "build."`). --- rewatch/src/build.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rewatch/src/build.rs b/rewatch/src/build.rs index 6254de3124f..525a925c927 100644 --- a/rewatch/src/build.rs +++ b/rewatch/src/build.rs @@ -128,7 +128,7 @@ pub fn get_compiler_info(project_context: &ProjectContext) -> Result, filter: &Option, @@ -239,7 +239,7 @@ impl fmt::Display for IncrementalBuildError { } } -#[instrument(name = "incremental_build", skip_all, fields(module_count = build_state.modules.len()))] +#[instrument(name = "build.incremental", skip_all, fields(module_count = build_state.modules.len()))] pub fn incremental_build( build_state: &mut BuildCommandState, default_timing: Option, From f870077ca9516dcd854f696c9ce0f2eaa2275ee6 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:06:11 +0200 Subject: [PATCH 13/19] Rewatch telemetry: encapsulate otel_enabled behind an accessor Expose via a `otel_enabled()` method instead of a public field. Lets us evolve the internal representation (e.g. caching the reason a provider failed to build) without breaking callers. --- rewatch/src/main.rs | 2 +- rewatch/src/telemetry.rs | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/rewatch/src/main.rs b/rewatch/src/main.rs index 2c089627b41..a8c038e5813 100644 --- a/rewatch/src/main.rs +++ b/rewatch/src/main.rs @@ -12,7 +12,7 @@ use rescript::{ fn main() { // Initialize telemetry (only active if OTEL_EXPORTER_OTLP_ENDPOINT is set) let telemetry_guard = telemetry::init_telemetry(); - let otel_enabled = telemetry_guard.otel_enabled; + let otel_enabled = telemetry_guard.otel_enabled(); let exit_code = run_main(otel_enabled); diff --git a/rewatch/src/telemetry.rs b/rewatch/src/telemetry.rs index 34859b6c7a4..75e0e5bd786 100644 --- a/rewatch/src/telemetry.rs +++ b/rewatch/src/telemetry.rs @@ -16,8 +16,15 @@ use tracing_subscriber::util::SubscriberInitExt; /// Drop this at the end of main to flush all spans. pub struct TelemetryGuard { provider: Option, - /// Whether OTEL tracing was initialized (affects logger setup in main) - pub otel_enabled: bool, + /// Whether OTEL tracing was initialized (affects logger setup in main). + otel_enabled: bool, +} + +impl TelemetryGuard { + /// Returns whether OTLP span export was successfully initialized. + pub fn otel_enabled(&self) -> bool { + self.otel_enabled + } } impl Drop for TelemetryGuard { From e064ffe89d67e8e22990f5541495820766c4cac4 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:06:45 +0200 Subject: [PATCH 14/19] Rewatch telemetry: use Path::file_name for PPX span attribute Replace the ad-hoc split-by-/-or-backslash + take-last-two-parts logic with Path::file_name, which handles platform separators correctly and is simpler to reason about. The attribute is informational only, so reducing to the file name (e.g. `ppx.exe`) is strictly more readable than the previous two-segment join. --- rewatch/src/build/parse.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/rewatch/src/build/parse.rs b/rewatch/src/build/parse.rs index e963810c2b4..c234f4c21db 100644 --- a/rewatch/src/build/parse.rs +++ b/rewatch/src/build/parse.rs @@ -352,9 +352,13 @@ fn make_parse_file_span( let mut experimental: Vec = Vec::new(); for pair in parser_args.windows(2) { if pair[0] == "-ppx" { - let parts: Vec<&str> = pair[1].split(['/', '\\']).collect(); - let start = if parts.len() >= 2 { parts.len() - 2 } else { 0 }; - ppx_names.push(parts[start..].join("/")); + // Use the PPX executable's file name so paths like + // `node_modules/foo/ppx.exe` reduce to `ppx.exe` cross-platform. + let name = Path::new(&pair[1]) + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| pair[1].clone()); + ppx_names.push(name); } else if pair[0] == "-enable-experimental" { experimental.push(pair[1].clone()); } From 94c87418ea486d227c00c921a9a3d2e22d1bdf2e Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:07:13 +0200 Subject: [PATCH 15/19] Rewatch telemetry: gate dirty-module count in build.parse span The dirty_modules attribute on build.parse required a full iteration over build_state.modules every parse phase, even when no subscriber was listening. Gate behind tracing::enabled!(INFO) so incremental rebuilds don't eat that O(N) scan when telemetry is off. --- rewatch/src/build/parse.rs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/rewatch/src/build/parse.rs b/rewatch/src/build/parse.rs index c234f4c21db..0945f30d352 100644 --- a/rewatch/src/build/parse.rs +++ b/rewatch/src/build/parse.rs @@ -21,19 +21,24 @@ pub fn generate_asts( let mut has_failure = false; let mut stderr = "".to_string(); - // Count dirty modules for the span - let dirty_modules = build_state - .modules - .values() - .filter(|m| match &m.source_type { - SourceType::SourceFile(sf) => { - sf.implementation.parse_dirty || sf.interface.as_ref().is_some_and(|i| i.parse_dirty) - } - SourceType::MlMap(mlmap) => mlmap.parse_dirty, - }) - .count(); - - let parse_span = info_span!("build.parse", dirty_modules = dirty_modules); + // Count dirty modules for the span attribute only when a subscriber is + // listening. Otherwise iterating every module every parse phase is pure + // waste on the hot path. + let parse_span = if tracing::enabled!(tracing::Level::INFO) { + let dirty_modules = build_state + .modules + .values() + .filter(|m| match &m.source_type { + SourceType::SourceFile(sf) => { + sf.implementation.parse_dirty || sf.interface.as_ref().is_some_and(|i| i.parse_dirty) + } + SourceType::MlMap(mlmap) => mlmap.parse_dirty, + }) + .count(); + info_span!("build.parse", dirty_modules = dirty_modules) + } else { + tracing::Span::none() + }; let _span = parse_span.enter(); build_state From d9613caae3beb4dca1eb40139dbb165cb4c168b3 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:07:53 +0200 Subject: [PATCH 16/19] Rewatch telemetry: add unit tests for init paths Smoke-test the no-op path and the Drop behavior: - init_noop returns otel_enabled() == false - The Drop impl is safe to run with provider = None (both explicit and implicit drops) - init_telemetry with no endpoint env vars returns the no-op guard Doesn't exercise the full OTLP init path (which would require a running collector) but catches regressions in the enable/disable branching and Drop safety. --- rewatch/src/telemetry.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/rewatch/src/telemetry.rs b/rewatch/src/telemetry.rs index 75e0e5bd786..0af6c20b5e7 100644 --- a/rewatch/src/telemetry.rs +++ b/rewatch/src/telemetry.rs @@ -125,3 +125,40 @@ fn init_noop() -> TelemetryGuard { otel_enabled: false, } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn noop_guard_reports_otel_disabled() { + let guard = init_noop(); + assert!(!guard.otel_enabled()); + // Explicit drop — must not panic with provider == None. + drop(guard); + } + + #[test] + fn noop_guard_drops_cleanly_without_explicit_shutdown() { + // Implicit drop at end of scope: exercises Drop with no provider. + let _guard = init_noop(); + } + + #[test] + fn init_telemetry_without_env_is_noop() { + // SAFETY: These env vars are process-global; tests run serially in a + // single thread by default for this crate (no #[test(flavor = ...)]). + // If parallelised later, wrap in a Mutex. + // Ensure the env is clean for this check. + unsafe { + std::env::remove_var("OTEL_EXPORTER_OTLP_ENDPOINT"); + std::env::remove_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"); + } + + let guard = init_telemetry(); + assert!( + !guard.otel_enabled(), + "expected no-op guard when OTEL endpoint env vars are unset" + ); + } +} From e67e1f071e5f6f86208d18095c90fc8913596353 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:08:21 +0200 Subject: [PATCH 17/19] Rewatch telemetry: document honored OTEL environment variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a table to the OpenTelemetry section listing the env vars rewatch responds to and what each does. All config is via standard OTEL vars — no rewatch-specific knobs — so this is the full surface area. --- AGENTS.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 9dcf7481731..4408a02822b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -430,6 +430,19 @@ Open http://localhost:16686 to view traces in the Jaeger UI. Note: Use `tracing::debug!` (not `log::debug!`) for events you want to appear in OTEL traces — they use separate logging systems. +##### Honored environment variables + +Rewatch follows the OTEL spec for configuration — no rewatch-specific knobs exist. + +| Variable | Purpose | +|---|---| +| `OTEL_EXPORTER_OTLP_ENDPOINT` | Base endpoint of the collector (e.g. `http://localhost:4318`). `/v1/traces` is appended for the trace exporter. Setting this (or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) is what enables telemetry — if neither is set, tracing is a no-op. | +| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Full trace endpoint used verbatim. Overrides the general endpoint for traces. | +| `OTEL_EXPORTER_OTLP_HEADERS` | Extra headers on exporter requests (e.g. `authorization=Bearer xyz`). | +| `OTEL_SERVICE_NAME` | Service name reported on spans. Defaults to `rewatch`. | +| `OTEL_RESOURCE_ATTRIBUTES` | Comma-separated `key=value` pairs added as resource attributes (e.g. `deployment.environment=ci,host.name=$HOSTNAME`). | +| `RUST_LOG` | Controls which span/event levels are captured (e.g. `RUST_LOG=info`, `RUST_LOG=rewatch=debug`). Defaults to `debug` when telemetry is enabled. | + #### Running Rewatch Directly When running the rewatch binary directly (via `cargo run` or the compiled binary) during development, you need to set environment variables to point to the local compiler and runtime. Otherwise, rewatch will try to use the installed versions: From 925f13a2fec14ccb7cd17f8056be2b7e66dcd1d0 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:24:38 +0200 Subject: [PATCH 18/19] Rewatch telemetry: move CLI-command spans onto their real functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The run_compiler_args / run_build / run_watch / run_clean / run_format wrappers in main.rs existed only as attachment points for #[instrument] — every trace had an outer 'rewatch.*' span whose one job was to re-enter the inner function. Move #[instrument(name = "rewatch.*")] directly onto build::build, build::get_compiler_args, build::clean::clean, watcher::start, and format::format. The spans now fire for any caller of those functions (not just the CLI dispatch), the trace tree loses a redundant layer, and main.rs drops ~80 lines of wrapper boilerplate for a single `exit_code` helper that maps Result -> process exit code. Span names and attributes are unchanged from the prior wrappers. --- rewatch/Cargo.lock | 170 +++++++++++++++++++++---------------- rewatch/Cargo.toml | 16 ++-- rewatch/src/build.rs | 2 + rewatch/src/build/clean.rs | 2 +- rewatch/src/format.rs | 2 +- rewatch/src/main.rs | 123 ++++++++++----------------- rewatch/src/watcher.rs | 1 + 7 files changed, 151 insertions(+), 165 deletions(-) diff --git a/rewatch/Cargo.lock b/rewatch/Cargo.lock index 898a12bbb32..2a16bdc86ed 100644 --- a/rewatch/Cargo.lock +++ b/rewatch/Cargo.lock @@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.3.3", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -140,6 +140,15 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -299,12 +308,25 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "ctrlc" -version = "3.4.7" +version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ + "dispatch2", "nix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.9.1", + "block2", + "libc", + "objc2", ] [[package]] @@ -350,7 +372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -484,17 +506,6 @@ dependencies = [ "slab", ] -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.3.3" @@ -507,12 +518,6 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "heck" version = "0.5.0" @@ -835,9 +840,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libredox" @@ -908,9 +913,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.30.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ "bitflags 2.9.1", "cfg-if", @@ -952,7 +957,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -971,6 +976,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "once_cell" version = "1.21.3" @@ -985,23 +1005,23 @@ checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "opentelemetry" -version = "0.24.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c365a63eec4f55b7efeceb724f1336f26a9cf3427b70e59e2cd2a5b947fba96" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" dependencies = [ "futures-core", "futures-sink", "js-sys", - "once_cell", "pin-project-lite", "thiserror", + "tracing", ] [[package]] name = "opentelemetry-http" -version = "0.13.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad31e9de44ee3538fb9d64fe3376c1362f406162434609e79aea2a41a0af78ab" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", @@ -1012,12 +1032,10 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" -version = "0.17.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b925a602ffb916fb7421276b86756027b37ee708f9dce2dbdcc51739f07e727" +checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" dependencies = [ - "async-trait", - "futures-core", "http", "opentelemetry", "opentelemetry-http", @@ -1030,35 +1048,30 @@ dependencies = [ [[package]] name = "opentelemetry-proto" -version = "0.7.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ee9f20bff9c984511a02f082dc8ede839e4a9bf15cc2487c8d6fea5ad850d9" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" dependencies = [ "opentelemetry", "opentelemetry_sdk", "prost", "tonic", + "tonic-prost", ] [[package]] name = "opentelemetry_sdk" -version = "0.24.1" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692eac490ec80f24a17828d49b40b60f5aeaccdfe6a503f939713afd22bc28df" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" dependencies = [ - "async-trait", "futures-channel", "futures-executor", "futures-util", - "glob", - "once_cell", "opentelemetry", "percent-encoding", "rand", - "serde_json", "thiserror", - "tokio", - "tokio-stream", ] [[package]] @@ -1134,9 +1147,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", "prost-derive", @@ -1144,9 +1157,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", "itertools", @@ -1172,20 +1185,19 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.8.5" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", @@ -1193,11 +1205,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.2.17", + "getrandom", ] [[package]] @@ -1322,7 +1334,6 @@ dependencies = [ "serde_path_to_error", "sysinfo", "tempfile", - "tokio", "tracing", "tracing-opentelemetry", "tracing-subscriber", @@ -1338,7 +1349,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1539,7 +1550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", @@ -1556,18 +1567,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1619,9 +1630,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.12.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" dependencies = [ "async-trait", "base64", @@ -1631,13 +1642,24 @@ dependencies = [ "http-body-util", "percent-encoding", "pin-project", - "prost", + "sync_wrapper", "tokio-stream", "tower-layer", "tower-service", "tracing", ] +[[package]] +name = "tonic-prost" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.3" @@ -1728,14 +1750,12 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.25.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9784ed4da7d921bc8df6963f8c80a0e4ce34ba6ba76668acadd3edbd985ff3b" +checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" dependencies = [ "js-sys", - "once_cell", "opentelemetry", - "opentelemetry_sdk", "smallvec", "tracing", "tracing-core", @@ -1746,9 +1766,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -1969,7 +1989,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/rewatch/Cargo.toml b/rewatch/Cargo.toml index 66e5e81f35e..15a555a85fa 100644 --- a/rewatch/Cargo.toml +++ b/rewatch/Cargo.toml @@ -37,17 +37,17 @@ tempfile = "3.10.1" # OpenTelemetry for tracing tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tracing-opentelemetry = "0.25" -opentelemetry = "0.24" -opentelemetry_sdk = { version = "0.24", features = ["rt-tokio"] } -# default-features disabled to skip the grpc-tonic stack (tonic/tower/axum/h2); -# we only use the HTTP/protobuf exporter via reqwest. -opentelemetry-otlp = { version = "0.17", default-features = false, features = [ +tracing-opentelemetry = "0.32" +opentelemetry = "0.31" +opentelemetry_sdk = "0.31" +# default-features disabled to skip metrics/logs support and the internal-logs +# feature we don't need. We only use HTTP/protobuf trace export via blocking +# reqwest — no Tokio runtime required. +opentelemetry-otlp = { version = "0.31", default-features = false, features = [ "http-proto", - "reqwest-client", + "reqwest-blocking-client", "trace", ] } -tokio = { version = "1", features = ["rt-multi-thread"] } [profile.release] codegen-units = 1 diff --git a/rewatch/src/build.rs b/rewatch/src/build.rs index 525a925c927..66ea61743fb 100644 --- a/rewatch/src/build.rs +++ b/rewatch/src/build.rs @@ -57,6 +57,7 @@ pub struct CompilerArgs { pub parser_args: Vec, } +#[instrument(name = "rewatch.compiler_args", skip_all, fields(file_path = %rescript_file_path.display()))] pub fn get_compiler_args(rescript_file_path: &Path) -> Result { let filename = &helpers::get_abs_path(rescript_file_path); let current_package = helpers::get_abs_path( @@ -488,6 +489,7 @@ pub fn write_build_ninja(build_state: &BuildCommandState) { } #[allow(clippy::too_many_arguments)] +#[instrument(name = "rewatch.build", skip_all, fields(working_dir = %path.display()))] pub fn build( filter: &Option, path: &Path, diff --git a/rewatch/src/build/clean.rs b/rewatch/src/build/clean.rs index 40e38d4f7bd..2ecc4de32fb 100644 --- a/rewatch/src/build/clean.rs +++ b/rewatch/src/build/clean.rs @@ -334,7 +334,7 @@ pub fn cleanup_after_build(build_state: &BuildCommandState) { }); } -#[instrument(name = "clean.clean", skip_all)] +#[instrument(name = "rewatch.clean", skip_all, fields(working_dir = %path.display()))] pub fn clean(path: &Path, show_progress: bool, plain_output: bool, prod: bool) -> Result<()> { let project_context = ProjectContext::new(path)?; let compiler_info = build::get_compiler_info(&project_context)?; diff --git a/rewatch/src/format.rs b/rewatch/src/format.rs index 1b318d0dad3..1c70d95987d 100644 --- a/rewatch/src/format.rs +++ b/rewatch/src/format.rs @@ -13,7 +13,7 @@ use crate::build::packages; use crate::cli::FileExtension; use clap::ValueEnum; -#[instrument(name = "format.format", skip_all)] +#[instrument(name = "rewatch.format", skip_all, fields(check, is_stdin = stdin_extension.is_some()))] pub fn format(stdin_extension: Option, check: bool, files: Vec) -> Result<()> { let bsc_path = helpers::get_bsc(); diff --git a/rewatch/src/main.rs b/rewatch/src/main.rs index a8c038e5813..27fa64a3106 100644 --- a/rewatch/src/main.rs +++ b/rewatch/src/main.rs @@ -1,7 +1,7 @@ +use anyhow::Result; use console::Term; use log::LevelFilter; use std::{io::Write, path::Path}; -use tracing::instrument; use rescript::{ build, cli, cmd, format, @@ -59,96 +59,59 @@ fn run_main(otel_enabled: bool) -> i32 { let show_progress = log_level_filter == LevelFilter::Info; match cli.command { - cli::Command::CompilerArgs { path } => run_compiler_args(&path), - cli::Command::Build(build_args) => run_build(build_args, show_progress, plain_output), + cli::Command::CompilerArgs { path } => { + exit_code(build::get_compiler_args(Path::new(&path)).map(|args| println!("{}", args))) + } + cli::Command::Build(build_args) => { + let result = build::build( + &build_args.filter, + Path::new(&build_args.folder as &str), + show_progress, + build_args.no_timing, + true, // create_sourcedirs is now always enabled + plain_output, + (*build_args.warn_error).clone(), + build_args.prod, + ); + if result.is_ok() + && let Some(args_after_build) = (*build_args.after_build).clone() + { + cmd::run(args_after_build); + } + exit_code(result.map(|_| ())) + } cli::Command::Watch(watch_args) => { let _lock = get_lock_or_exit(LockKind::Watch, &watch_args.folder); - run_watch(watch_args, show_progress, plain_output) + exit_code(watcher::start( + &watch_args.filter, + show_progress, + &watch_args.folder, + (*watch_args.after_build).clone(), + true, // create_sourcedirs is now always enabled + plain_output, + (*watch_args.warn_error).clone(), + watch_args.prod, + )) } cli::Command::Clean { folder, prod } => { let _lock = get_lock_or_exit(LockKind::Build, &folder); - let code = run_clean(&folder, show_progress, plain_output, prod); + let code = exit_code(build::clean::clean( + Path::new(&folder as &str), + show_progress, + plain_output, + prod, + )); let _ = drop_lock(LockKind::Build, &folder); code } - cli::Command::Format { stdin, check, files } => run_format(stdin, check, files), - } -} - -#[instrument(name = "rewatch.compiler_args", skip_all, fields(file_path = %path))] -fn run_compiler_args(path: &str) -> i32 { - match build::get_compiler_args(Path::new(path)) { - Ok(args) => { - println!("{}", args); - 0 - } - Err(e) => { - eprintln!("{:#}", e); - 1 - } - } -} - -#[instrument(name = "rewatch.build", skip_all, fields(working_dir = %build_args.folder.folder))] -fn run_build(build_args: cli::BuildArgs, show_progress: bool, plain_output: bool) -> i32 { - match build::build( - &build_args.filter, - Path::new(&build_args.folder as &str), - show_progress, - build_args.no_timing, - true, // create_sourcedirs is now always enabled - plain_output, - (*build_args.warn_error).clone(), - build_args.prod, - ) { - Err(e) => { - eprintln!("{:#}", e); - 1 - } - Ok(_) => { - if let Some(args_after_build) = (*build_args.after_build).clone() { - cmd::run(args_after_build) - } - 0 - } - } -} - -#[instrument(name = "rewatch.watch", skip_all, fields(working_dir = %watch_args.folder.folder))] -fn run_watch(watch_args: cli::WatchArgs, show_progress: bool, plain_output: bool) -> i32 { - match watcher::start( - &watch_args.filter, - show_progress, - &watch_args.folder, - (*watch_args.after_build).clone(), - true, // create_sourcedirs is now always enabled - plain_output, - (*watch_args.warn_error).clone(), - watch_args.prod, - ) { - Err(e) => { - eprintln!("{:#}", e); - 1 - } - Ok(_) => 0, - } -} - -#[instrument(name = "rewatch.clean", skip_all, fields(working_dir = %folder))] -fn run_clean(folder: &str, show_progress: bool, plain_output: bool, prod: bool) -> i32 { - match build::clean::clean(Path::new(folder), show_progress, plain_output, prod) { - Ok(_) => 0, - Err(e) => { - eprintln!("{:#}", e); - 1 - } + cli::Command::Format { stdin, check, files } => exit_code(format::format(stdin, check, files)), } } -#[instrument(name = "rewatch.format", skip_all, fields(check = check, is_stdin = stdin.is_some()))] -fn run_format(stdin: Option, check: bool, files: Vec) -> i32 { - match format::format(stdin, check, files) { - Ok(_) => 0, +/// Map a command's Result<()> to a process exit code, printing the error on failure. +fn exit_code(result: Result<()>) -> i32 { + match result { + Ok(()) => 0, Err(e) => { eprintln!("{:#}", e); 1 diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index dd904913f0b..130a62fec16 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -479,6 +479,7 @@ async fn async_watch( } #[allow(clippy::too_many_arguments)] +#[tracing::instrument(name = "rewatch.watch", skip_all, fields(working_dir = %folder))] pub fn start( filter: &Option, show_progress: bool, From a0429522fe0a979973cabaaac611aa0301479a9b Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Sun, 19 Apr 2026 15:26:37 +0200 Subject: [PATCH 19/19] Rewatch: impl AsRef for FolderArg Lets callers pass folder arguments to functions taking &Path without the cryptic `as &str` deref-coercion cast: Path::new(&build_args.folder as &str) -> build_args.folder.as_ref() FolderArg already had a Deref impl, so this is the analogous addition for the &Path direction. --- rewatch/src/cli.rs | 8 +++++++- rewatch/src/main.rs | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/rewatch/src/cli.rs b/rewatch/src/cli.rs index 07c3cdfb9bb..60b4ebffe7b 100644 --- a/rewatch/src/cli.rs +++ b/rewatch/src/cli.rs @@ -11,7 +11,7 @@ // // However, we may want to revisit the decision to use clap after the v12 release. -use std::{env, ffi::OsString, ops::Deref}; +use std::{env, ffi::OsString, ops::Deref, path::Path}; use clap::{Args, CommandFactory, Parser, Subcommand, error::ErrorKind}; use clap_verbosity_flag::InfoLevel; @@ -496,6 +496,12 @@ impl Deref for FolderArg { } } +impl AsRef for FolderArg { + fn as_ref(&self) -> &Path { + Path::new(&self.folder) + } +} + impl Deref for FilterArg { type Target = Option; diff --git a/rewatch/src/main.rs b/rewatch/src/main.rs index 27fa64a3106..9bafc605332 100644 --- a/rewatch/src/main.rs +++ b/rewatch/src/main.rs @@ -65,7 +65,7 @@ fn run_main(otel_enabled: bool) -> i32 { cli::Command::Build(build_args) => { let result = build::build( &build_args.filter, - Path::new(&build_args.folder as &str), + build_args.folder.as_ref(), show_progress, build_args.no_timing, true, // create_sourcedirs is now always enabled @@ -96,7 +96,7 @@ fn run_main(otel_enabled: bool) -> i32 { cli::Command::Clean { folder, prod } => { let _lock = get_lock_or_exit(LockKind::Build, &folder); let code = exit_code(build::clean::clean( - Path::new(&folder as &str), + folder.as_ref(), show_progress, plain_output, prod,