Crustrace
Crustrace is a procedural macro that automatically instruments all functions in a module with tracing spans, eliminating the need to manually add #[instrument] to every function.
Use Crustrace when you want comprehensive tracing with minimal effort to add and remove.
Stick with manual instrumentation when you need fine-grained control over which functions are traced.
Motivation
When adding distributed tracing to Rust applications, developers typically need to annotate every function they want to trace:
This is tedious and a barrier to quick instrumentation of anything more than a function or two (we really want module and crate-level instrumentation).
Crustrace solves this by automatically instrumenting all functions in a module, giving you complete call-chain tracing with minimal code changes. It's a simple initial solution but would be extensible to filter the functions it's applied to by name, by crate in a workspace, and so on.
Installation
Add Crustrace to your Cargo.toml:
[]
= "0.1"
= "0.1"
= "0.3"
Usage
Basic Usage
Apply the #[omni] attribute to any module:
- See the example in
examples/omni_mod_fiband alongside it as a Rust-script
use omni;
or more typically, by putting #![omni] (note the !) at the top of a module not declared by a mod block.
All functions in the module are then automatically instrumented as if you had written:
Instrumenting Impl Blocks
Crustrace also works on impl blocks, automatically instrumenting all methods:
- See the example in
examples/omni_struct_fiband alongside it as a Rust-script
use omni;
;
All methods in the impl block get automatically instrumented, including:
- Constructor methods like
new() - Public methods with parameters
- Private helper methods
- Methods with generic parameters
This gives you complete tracing of method calls within your types.
WORK IN PROGRESS
crustrace::instrument is a syn-free (simpler, yet functional!) version
of the tracing-attributes::instrument macro.
In turn, crustrace::omni no longer uses tracing::instrument, it is
entirely using crustrace::instrument.
Complete Example
use omni;
use tracing_subscriber;
// Initialize tracing
fmt
.with_max_level
.with_span_events
.init;
use *;
let result = fibonacci;
println!;
This produces detailed tracing output showing the complete call hierarchy:
INFO fibonacci{n=5}: enter
INFO fibonacci{n=4}: enter
INFO fibonacci{n=3}: enter
INFO fibonacci{n=2}: enter
INFO fibonacci{n=1}: enter
INFO fibonacci{n=1}: return=1
INFO fibonacci{n=1}: exit
INFO fibonacci{n=0}: enter
INFO fibonacci{n=0}: return=0
INFO fibonacci{n=0}: exit
INFO add_numbers{a=1 b=0}: enter
INFO add_numbers{a=1 b=0}: return=1
INFO add_numbers{a=1 b=0}: exit
INFO fibonacci{n=2}: return=1
INFO fibonacci{n=2}: exit
// ... and so on
How It Works
Crustrace uses a procedural macro to parse the token stream of a module and automatically inject #[tracing::instrument(level = "info", ret)] before every function definition.
It uses proc-macro2 (it's free of syn!) to
parse tokens rather than doing string replacement or full on AST creation.
Implementation Details
The macro:
- Parses tokens rather than doing string replacement to avoid false positives
- Validates function definitions by ensuring
fnis followed by an identifier - Preserves existing attributes and doesn't interfere with other procedural macros
- Handles edge cases like generic functions, async functions, and various formatting styles
What Gets Instrumented
- Regular functions:
fn foo() { ... } - Generic functions:
fn foo<T>(x: T) { ... } - Functions with complex signatures:
fn foo(x: impl Display) -> Result<String, Error> { ... } - Methods in impl blocks:
impl MyStruct { fn method(&self) { ... } } - Generic impl blocks:
impl<T> Container<T> { ... }
What Doesn't Get Instrumented
- Function calls within expressions:
some_fn_call() - String literals containing "fn":
"fn not a function" - Comments:
// fn something - Already instrumented functions (won't double-instrument)
Configuration
By default, Crustrace applies these instrument settings:
- Level:
info - Return values: Logged (
ret) - Parameters: All function parameters are automatically captured
Future versions may support customising these settings via macro parameters (please feel free to suggest ideas and submit at least some test for it if you can't figure out how it'd be implemented). PRs would be ideal!
Performance Considerations
Tracing Overhead
- Instrumented functions have minimal overhead when tracing is disabled
- When tracing is enabled, overhead is proportional to the number of function calls
- It might be wise to consider using
RUST_LOGenvironment variable to control tracing levels in future
Compilation Impact
- Crustrace processes modules at compile time with minimal impact on build performance
- The generated code is equivalent to manually writing
#[instrument]attributes - No runtime dependencies beyond the standard
tracingcrate
Ideas
- Depth limits
- Specified crates/function patterns to trace
- Environment variable enabled tracing
- More nuanced control of traced events, log level, etc.
License
This project is licensed under either of:
- Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.