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 fields

By 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 fields

skip_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 recorded

skip(...) 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 recorded

Use 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 data

skip_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 skipped

List 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 paths

skip_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 manually

skip 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 fields

You 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 parameter

skip_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 Debug

Non-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 enabled

skip 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 fields

Each #[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.