Loading page…
Rust walkthroughs
Loading page…
tracing, how do #[instrument(skip_all)] and #[instrument(skip(...))] differ in what gets recorded?#[instrument(skip_all)] excludes all function parameters from the span's recorded fields, regardless of whether they implement Debug or contain sensitive data. #[instrument(skip(...))] explicitly lists specific parameters to skip, allowing selective control over which arguments are recorded and which are omitted. Both approaches prevent potentially expensive or sensitive data from appearing in traces, but skip_all is a blanket exclusion while skip(...) requires enumerating each parameter to exclude. The choice between them depends on whether you want to record any parameters by default—if you need most parameters recorded with a few exceptions, skip(...) is appropriate; if you want no parameters recorded, skip_all is simpler.
use tracing::{instrument, info};
#[instrument]
fn process_user(user_id: u64, name: String) {
info!("Processing user");
}
fn main() {
tracing_subscriber::fmt().init();
process_user(42, "Alice".to_string());
}
// Creates span: process_user{user_id=42 name="Alice"}
// All parameters are recorded as fieldsBy default, all function parameters become span fields.
use tracing::{instrument, info};
#[instrument(skip_all)]
fn process_user(user_id: u64, name: String) {
info!("Processing user");
}
fn main() {
tracing_subscriber::fmt().init();
process_user(42, "Alice".to_string());
}
// Creates span: process_user{}
// No parameters recorded as fieldsskip_all excludes every parameter from the span.
use tracing::{instrument, info};
#[instrument(skip(name))]
fn process_user(user_id: u64, name: String) {
info!("Processing user");
}
fn main() {
tracing_subscriber::fmt().init();
process_user(42, "Alice".to_string());
}
// Creates span: process_user{user_id=42}
// 'name' is skipped, 'user_id' is recordedskip(...) takes a comma-separated list of parameter names to exclude.
use tracing::{instrument, info};
struct User {
id: u64,
name: String,
password: String, // Sensitive!
}
// Skip sensitive fields
#[instrument(skip(user, api_key))]
fn authenticate(user: User, api_key: String) -> bool {
// Don't log sensitive data
user.id > 0
}
fn main() {
let user = User {
id: 1,
name: "Alice".into(),
password: "secret".into(),
};
authenticate(user, "api_key_123".into());
}
// Neither 'user' nor 'api_key' appear in span
// Only the function name is recordedUse skip to prevent sensitive data from appearing in logs.
use tracing::{instrument, info};
#[instrument(skip_all)]
fn process_payment(
account: String,
amount: f64,
card_number: String,
cvv: String,
) {
info!("Processing payment");
}
// All parameters skipped - nothing sensitive logged
// Safer when all parameters could contain sensitive dataskip_all is appropriate when all parameters are sensitive or unnecessary.
use tracing::{instrument, info};
#[instrument(skip(database, cache, config))]
fn fetch_data(
database: Database,
cache: Cache,
config: Config,
query: String, // Keep this one
) -> Result<String, Error> {
info!("Fetching data");
Ok(query)
}
// Only 'query' is recorded
// 'database', 'cache', 'config' are skippedList multiple parameters to skip in the parentheses.
use tracing::{instrument, info};
// Large data structure
struct BigData {
values: Vec<Vec<String>>, // Potentially huge
}
// Expensive: BigData::fmt is called even if trace level is disabled
#[instrument]
fn process_slow(data: BigData) {
info!("Processing");
}
// Better: Skip the expensive parameter
#[instrument(skip(data))]
fn process_fast(data: BigData) {
info!("Processing");
}
// skip avoids calling Debug::fmt on 'data'Skipping parameters avoids potentially expensive Debug formatting.
use tracing::{instrument, info};
#[instrument(skip_all)]
fn process_batch(
items: Vec<Item>, // Skip Debug
metadata: Metadata, // Skip Debug
context: Context, // Skip Debug
) {
info!("Processing batch");
}
// No Debug calls made for any parameter
// Maximum performance for hot pathsskip_all avoids all Debug overhead.
use tracing::{instrument, info};
#[instrument(skip(secret), fields(extra = tracing::field::empty()))]
fn process(user_id: u64, secret: String) {
info!("Processing");
}
// 'user_id' recorded (not in skip list)
// 'secret' skipped
// 'extra' field added manuallyskip and fields can be combined.
use tracing::{instrument, info, Span};
#[instrument(skip(user), fields(user.name = tracing::field::Empty))]
fn process_user(user: User) {
let span = Span::current();
span.record("user.name", &user.name);
info!("Processing user");
}
struct User {
id: u64,
name: String,
}
// 'user' is skipped (not Debug)
// 'user.name' is recorded via fieldsYou can add specific fields while skipping the whole parameter.
use tracing::{instrument, info};
use std::net::TcpStream;
// Type without Debug
struct Connection {
stream: TcpStream, // TcpStream doesn't implement Debug
}
// This would fail to compile:
// #[instrument]
// fn use_connection(conn: Connection) { }
// Error: Connection doesn't implement Debug
// Solution: skip the parameter
#[instrument(skip(conn))]
fn use_connection(conn: Connection) {
info!("Using connection");
}Parameters without Debug must be skipped or the macro fails to compile.
use tracing::{instrument, info};
struct DatabasePool; // No Debug impl
struct Cache; // No Debug impl
#[instrument(skip_all)] // Avoids Debug requirement entirely
fn get_data(pool: DatabasePool, cache: Cache, key: String) {
info!("Getting data");
}
// No Debug impl needed for any parameterskip_all is useful when multiple parameters lack Debug.
use tracing::{instrument, info};
struct Service {
id: u64,
name: String,
}
impl Service {
// 'self' is captured by default
#[instrument]
fn process(&self, input: String) {
info!("Processing");
}
// Span: process{self=Service { id: 42, name: "test" } input="data"}
// Skip self
#[instrument(skip(self))]
fn process_skip(&self, input: String) {
info!("Processing");
}
// Span: process_skip{input="data"}
}self is a regular parameter that can be skipped.
use tracing::{instrument, info};
struct Service {
connection: Connection,
}
impl Service {
#[instrument(skip_all)]
fn handle(&self, request: Request) {
info!("Handling request");
}
// Neither 'self' nor 'request' recorded
}skip_all on methods skips self and all other parameters.
use tracing::{instrument, info, Span};
#[instrument(skip(secret), fields(manual = "added"))]
fn process(public: u64, secret: String) {
let span = Span::current();
// Access recorded fields
// 'public' is available as a field
// 'secret' is not (skipped)
// 'manual' is available
info!("Fields recorded");
}Skipped parameters are not available as span fields.
use tracing::{instrument, info};
#[instrument]
fn traced_all(x: i32, y: i32) {
info!("done");
}
#[instrument(skip(y))]
fn traced_partial(x: i32, y: i32) {
info!("done");
}
#[instrument(skip_all)]
fn traced_none(x: i32, y: i32) {
info!("done");
}
fn main() {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
traced_all(1, 2);
// Span: traced_all{x=1 y=2}
traced_partial(1, 2);
// Span: traced_partial{x=1}
traced_none(1, 2);
// Span: traced_none{}
}The span fields differ based on skip configuration.
use tracing::{instrument, info};
// This struct has Debug
#[derive(Debug)]
struct DebugStruct {
value: i32,
}
// This struct has no Debug
struct NoDebugStruct {
value: i32,
}
#[instrument]
fn with_debug(data: DebugStruct) {
info!("done");
}
// Compiles: DebugStruct has Debug
#[instrument(skip(data))]
fn without_debug(data: NoDebugStruct) {
info!("done");
}
// Compiles: 'data' is skipped, no Debug needed
// #[instrument]
// fn fails(data: NoDebugStruct) { }
// Won't compile: NoDebugStruct doesn't implement DebugNon-Debug types must be skipped.
use tracing::{instrument, info};
// Use skip when:
// - Most parameters should be recorded
// - A few specific ones should be excluded
// - You want explicit control
#[instrument(skip(password, token))]
fn login(username: String, password: String, token: String) {
// 'username' recorded, 'password' and 'token' skipped
}
// Use skip_all when:
// - All parameters are sensitive
// - All parameters lack Debug
// - Recording nothing is the right default
// - Simplicity preferred over explicitness
#[instrument(skip_all)]
fn internal_process(db: Database, cache: Cache, config: Config) {
// Nothing recorded
}Use skip for selective exclusion, skip_all for blanket exclusion.
use tracing::{instrument, info};
#[instrument(skip(large_data), level = "debug")]
fn process(large_data: Vec<u8>, id: u64) {
info!("Processing");
}
// Even at debug level, 'large_data' is never formatted
// 'id' is only formatted if debug level is enabledskip prevents formatting regardless of log level.
use tracing::{instrument, info};
#[instrument]
fn outer(x: i32, y: i32) {
inner(x, y);
}
#[instrument(skip_all)]
fn inner(a: i32, b: i32) {
info!("Inner");
}
// outer span: outer{x=1 y=2}
// inner span: inner{}
// Each function has its own span with its own fieldsEach #[instrument] creates an independent span.
| Aspect | skip(...) | skip_all |
|--------|-------------|-----------|
| Excludes | Named parameters | All parameters |
| Includes | All other parameters | No parameters |
| Verbosity | Explicit list | No list needed |
| Use case | Selective exclusion | Complete exclusion |
| Debug required | For included params | For no params |
| Sensitive data | Exclude specific fields | Exclude all fields |
The #[instrument(skip(...))] and #[instrument(skip_all)] attributes provide different granularities of control over what gets recorded in spans:
skip(...): Explicitly list parameters to exclude. This is the right choice when you want most parameters recorded but need to exclude specific ones—typically sensitive data like passwords or tokens, or expensive-to-format types. The macro generates field recordings for all parameters not in the skip list. Parameters in the skip list are not referenced by the span at all, avoiding both the privacy concern and the Debug formatting cost.
skip_all: Exclude every parameter from the span. Use this when no parameters provide useful tracing information, all parameters are sensitive, or when you want maximum performance and don't need any parameter values in traces. This is common for internal functions where the function name alone provides enough context, or when all parameters are large data structures without meaningful Debug output.
Key insight: The primary difference is intent and maintenance. skip_all is a blanket "don't record parameters" policy—if you add a parameter later, it's automatically skipped too. skip(...) is explicit—if you add a new parameter, it will be recorded unless you add it to the skip list. For sensitive data handling, skip(...) is safer because you consciously exclude what shouldn't be logged. For performance-critical or internal-only code, skip_all reduces boilerplate and prevents accidental logging of new parameters.
Both approaches prevent Debug formatting overhead for skipped parameters, making them valuable for hot paths where the Debug implementation would be expensive. The choice comes down to whether you want any parameters logged by default.