Rewatch: add OpenTelemetry tracing support#8370
Open
jfrolich wants to merge 19 commits intorescript-lang:masterfrom
Open
Rewatch: add OpenTelemetry tracing support#8370jfrolich wants to merge 19 commits intorescript-lang:masterfrom
jfrolich wants to merge 19 commits intorescript-lang:masterfrom
Conversation
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 rescript-lang#8241.
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 rescript-lang#8241.
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 rescript-lang#8241.
a79a52c to
ece9e1b
Compare
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.
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.
…ile 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.
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.
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.
run_main only reads .otel_enabled once; the reference parameter added noise without benefit. Pass a bool instead.
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.
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."`).
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.
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.
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.
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.
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.
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.
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<Target = str> impl, so this is the analogous addition for the &Path direction.
rescript
@rescript/darwin-arm64
@rescript/darwin-x64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/runtime
@rescript/win32-x64
commit: |
rolandpeelen
approved these changes
Apr 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cherry-picked telemetry-only subset of #8241. Adds optional OTLP tracing export to rewatch, controlled by the
OTEL_EXPORTER_OTLP_ENDPOINTenvironment variable. When unset, tracing is a no-op.telemetrymodule wiring uptracing+opentelemetry-otlpwith a batch HTTP exporter and aTelemetryGuardthat flushes on drop.#[instrument]/info_span!coverage acrossinitialize_build,incremental_build,compile,parse,clean,packages,format, and the watcher full-compile trigger.build.parse_fileandbuild.compile_filespans nest correctly under their parents (these are otherwise orphaned because tracing spans are thread-local).ctrlc"termination" feature enabled so SIGTERM/SIGHUP also trigger the guard's Drop — required to flush buffered spans when stopped by a container/systemd/kill.AGENTS.md.The original PR also included a Vitest test harness, CI/Makefile/scripts changes, and two unrelated UX tweaks. Those are excluded here — the UX tweaks will follow in separate PRs.
Test plan
cargo build --release --manifest-path rewatch/Cargo.tomlcargo test --manifest-path rewatch/Cargo.toml(68/68 pass)cargo clippy --manifest-path rewatch/Cargo.toml --all-targets --all-features(clean)OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 ./rewatch buildwith Jaeger running🤖 Generated with Claude Code