When using 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.
Basic instrumentation Without Skipping
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.
Using skip_all to Exclude All Parameters
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.
Using skip with Specific Parameters
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.
Skipping Sensitive Data
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.
skip_all for Complete Privacy
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.
skip with Multiple Parameters
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.
Performance: Avoiding Debug Formatting
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.
skip_all Avoids All Debug Calls
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.
Combining skip with fields
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.
Adding fields Without Skipping
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.
Parameters Without Debug Implementation
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.
skip_all for Non-Debug Types
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.
Instrumenting Methods with self
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.
skip_all on Methods
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.
Viewing Recorded Fields
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.
Effect on Trace Output
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.
Debug Requirement for Recorded Fields
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.
Choosing Between skip and skip_all
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.
Level Filtering with skip
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.
Nested Spans with Different skip Settings
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.
Summary Table
| 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 |
Synthesis
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.
