Skip to content

Rewatch: add OpenTelemetry tracing support#8370

Open
jfrolich wants to merge 19 commits intorescript-lang:masterfrom
jfrolich:telemetry-cherry-pick
Open

Rewatch: add OpenTelemetry tracing support#8370
jfrolich wants to merge 19 commits intorescript-lang:masterfrom
jfrolich:telemetry-cherry-pick

Conversation

@jfrolich
Copy link
Copy Markdown
Collaborator

Summary

Cherry-picked telemetry-only subset of #8241. Adds optional OTLP tracing export to rewatch, controlled by the OTEL_EXPORTER_OTLP_ENDPOINT environment variable. When unset, tracing is a no-op.

  • New telemetry module wiring up tracing + opentelemetry-otlp with a batch HTTP exporter and a TelemetryGuard that flushes on drop.
  • #[instrument] / info_span! coverage across initialize_build, incremental_build, compile, parse, clean, packages, format, and the watcher full-compile trigger.
  • Parent-span plumbing through rayon so build.parse_file and build.compile_file spans 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.
  • Jaeger setup instructions in 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.toml
  • cargo test --manifest-path rewatch/Cargo.toml (68/68 pass)
  • cargo clippy --manifest-path rewatch/Cargo.toml --all-targets --all-features (clean)
  • Manual: OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 ./rewatch build with Jaeger running

🤖 Generated with Claude Code

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.
@jfrolich jfrolich force-pushed the telemetry-cherry-pick branch from a79a52c to ece9e1b Compare April 19, 2026 12:30
jfrolich added 15 commits April 19, 2026 15:00
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.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 19, 2026

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@8370

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@8370

@rescript/darwin-x64

npm i https://pkg.pr.new/@rescript/darwin-x64@8370

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@8370

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@8370

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@8370

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@8370

commit: a042952

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants