Loading page…
Rust walkthroughs
Loading page…
tracing's #[instrument(skip(...))] to exclude sensitive parameters from logs?The #[instrument] attribute macro in tracing automatically creates spans for functions and logs their parameters, but this convenience becomes a security risk when parameters contain sensitive data like passwords, API keys, or personal information. The skip argument excludes specified parameters from the span entirely, preventing them from appearing in logs. Additional control comes from skip_all, field redaction with fields(...), and implementing custom Display or Debug for types that need masked output.
use tracing::{info, instrument};
// Default behavior: all parameters are logged
#[instrument]
fn process_user(user_id: u64, username: &str) {
info!("Processing user");
}
// When called: process_user{user_id=123 username="alice"}
// All parameters appear in the spanBy default, #[instrument] captures all function parameters in the span.
use tracing::instrument;
#[instrument(skip(password, api_key))]
fn authenticate(username: &str, password: &str, api_key: &str) -> bool {
// password and api_key won't appear in logs
username == "admin" && password == "secret"
}
// When called: authenticate{username="admin"}
// password and api_key are excludedThe skip argument removes specified parameters from the span entirely.
use tracing::instrument;
#[instrument(skip(password, ssn, credit_card))]
fn create_account(
username: &str,
email: &str,
password: &str,
ssn: &str,
credit_card: &str,
) -> Result<(), String> {
// Only username and email appear in logs
Ok(())
}
fn example() {
create_account(
"alice",
"alice@example.com",
"secret_password",
"123-45-6789",
"4111-1111-1111-1111",
);
// Span: create_account{username="alice" email="alice@example.com"}
}List all sensitive parameters in the skip argument.
use tracing::instrument;
// Skip all parameters - useful when everything is sensitive
#[instrument(skip_all)]
fn process_payment(card_number: &str, cvv: &str, amount: f64) {
// No parameters logged
}
// Or skip_all and add back only safe ones
#[instrument(skip_all, fields(user_id = %user_id))]
fn transfer_funds(user_id: u64, from_account: &str, to_account: &str, amount: f64) {
// Only user_id appears, manually specified
}skip_all excludes all parameters, then you can add back specific fields.
use tracing::instrument;
#[instrument(skip(password), fields(username = %username, role = %role))]
fn login(username: &str, password: &str, role: &str) -> bool {
// Explicitly control what's logged and how
true
}
// Alternative: redact in the field assignment
#[instrument(skip(password))]
fn register(email: &str, password: &str) {
// email is logged automatically, password is skipped
}Use explicit fields to control formatting and add derived values.
use tracing::instrument;
use std::fmt;
struct Password(String);
impl Debug for Password {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Password(***)")
}
}
impl std::fmt::Display for Password {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "***")
}
}
// Even without skip, the masked version appears
#[instrument]
fn set_password(password: Password) {
// Logs: set_password{password=Password(***)}
}
// But skip is still recommended for security
#[instrument(skip(password))]
fn set_password_secure(password: Password) {
// Logs: set_password{}
}Implement Debug/Display to mask sensitive types, but prefer skip for safety.
use tracing::instrument;
use std::fmt;
struct UserCredentials {
username: String,
password: String,
api_key: String,
}
impl Debug for UserCredentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UserCredentials")
.field("username", &self.username)
.field("password", &"***")
.field("api_key", &"***")
.finish()
}
}
#[instrument(skip(credentials))]
fn validate_credentials(credentials: UserCredentials) -> bool {
// credentials is excluded entirely
credentials.username == "admin"
}
// Even without skip, debug implementation masks sensitive fields
#[instrument]
fn validate_credentials_debug(credentials: UserCredentials) -> bool {
// Logs: validate_credentials_debug{credentials=UserCredentials { username: "admin", password: "***", api_key: "***" }}
true
}Combine Debug masking with skip for defense in depth.
use tracing::instrument;
struct Database {
connection_string: String,
pool_size: usize,
}
impl Database {
// Skip sensitive fields of self
#[instrument(skip(self))]
fn query(&self, sql: &str) -> Vec<String> {
// self is excluded entirely
vec![]
}
// Access specific fields manually
#[instrument(skip(self), fields(pool_size = self.pool_size))]
fn status(&self) -> String {
// Only pool_size is logged
format!("Pool size: {}", self.pool_size)
}
// Skip sensitive fields while keeping others
#[instrument(skip(self.connection_string))]
fn connect(&self) -> bool {
// self.connection_string skipped, but self logged
true
}
}Use skip(self) or skip(self.field) for methods with sensitive data.
use tracing::instrument;
#[instrument(skip(password))]
async fn authenticate_async(username: &str, password: &str) -> Result<String, String> {
// password excluded from span
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
if username == "admin" && password == "secret" {
Ok("token".to_string())
} else {
Err("invalid credentials".to_string())
}
}
// Works with async, including the async keyword
#[instrument(skip_all, fields(username = %username))]
async fn process_request(username: &str, sensitive_data: &str) {
// Only manually added username field appears
}skip works identically with async functions.
use tracing::instrument;
#[instrument(skip(password), ret)]
fn login_with_return(username: &str, password: &str) -> bool {
username == "admin" && password == "secret"
}
// Logs return value: login_with_return{username="admin"} => true
#[instrument(skip(password), err)]
fn login_with_error(username: &str, password: &str) -> Result<String, String> {
if password == "secret" {
Ok("token".to_string())
} else {
Err("invalid".to_string())
}
}
// Logs error: login_with_error{username="admin"} err="invalid"
#[instrument(skip(password), ret, err)]
fn login_full(username: &str, password: &str) -> Result<String, String> {
if password == "secret" {
Ok("token".to_string())
} else {
Err("invalid".to_string())
}
}
// Logs both return and errorUse ret and err to log return values, being careful not to leak sensitive data.
use tracing::instrument;
struct Session {
token: String,
user_id: u64,
}
#[instrument(skip(password), fields(user_id = session.user_id))]
fn create_session(username: &str, password: &str) -> Session {
Session {
token: "secret_token".to_string(),
user_id: 123,
}
}
// Don't use ret with sensitive return values
// This would leak the token:
// #[instrument(skip(password), ret)]
// fn create_session(...) -> Session { ... }Be cautious with ret when return values contain sensitive data.
use tracing::instrument;
#[instrument(skip(api_key))]
fn outer(api_key: &str, data: &str) {
inner(data);
}
#[instrument(skip(data))]
fn inner(data: &str) {
// Each function has its own span
// outer{api_key="[REDACTED]"}
// inner{data="[REDACTED]"}
}
// Nested spans create a hierarchy in traces
#[instrument(skip(api_key, user_password))]
fn process_request(api_key: &str, user_password: &str, request_id: u64) {
validate_api_key(api_key);
authenticate_user(user_password);
}
#[instrument(skip(key))]
fn validate_api_key(key: &str) {
// Nested under process_request span
}
#[instrument(skip(password))]
fn authenticate_user(password: &str) {
// Nested under process_request span
}Each instrumented function creates its own span in the hierarchy.
use tracing::instrument;
#[instrument(skip(password), level = "debug")]
fn debug_auth(username: &str, password: &str) {
// Only logged when debug level is enabled
}
#[instrument(skip(password), level = "trace")]
fn trace_auth(username: &str, password: &str) {
// Only logged when trace level is enabled
}
#[instrument(skip(password), skip_all)]
fn skip_multiple(username: &str, password: &str, token: &str) {
// skip and skip_all can combine
}
#[instrument(skip(password, token))] // Comma-separated
fn skip_comma_separated(username: &str, password: &str, token: &str) {
// Multiple skips separated by comma
}Control logging verbosity with level, combine skip arguments as needed.
use tracing::instrument;
use secrecy::{Secret, ExposeSecret};
// Using secrecy crate for compile-time protection
#[instrument(skip(password))]
fn login_with_secret(username: &str, password: Secret<String>) -> bool {
let pwd = password.expose_secret();
username == "admin" && pwd == "secret"
}
// Pattern: public ID, private data
#[instrument(skip(credit_card, ssn), fields(user_id = %user_id))]
fn update_user_profile(
user_id: u64,
name: &str,
credit_card: &str,
ssn: &str,
) {
// Only user_id and name logged
}
// Pattern: skip sensitive, add safe summary
#[instrument(skip(payload), fields(payload_size = payload.len()))]
fn process_webhook(payload: &str) {
// Logs payload size but not contents
}
// Pattern: redact part of value
#[instrument(skip(token), fields(token_prefix = &token[..8.min(token.len())]))]
fn use_token(token: &str) {
// Logs first 8 chars of token only
}These patterns help maintain security while preserving useful debug information.
use tracing::instrument;
// BAD: Logging sensitive data
#[instrument]
fn login_bad(username: &str, password: &str) {
// password appears in logs!
}
// BAD: Logging sensitive return values
#[instrument(skip(password), ret)]
fn create_token_bad(password: &str) -> String {
"secret_token".to_string()
// The token is logged!
}
// BAD: Partially sensitive struct without masking
struct User {
name: String,
password: String, // Will be logged
}
#[instrument]
fn process_user_bad(user: User) {
// password field appears in logs
}
// GOOD: Skip or mask everything sensitive
#[instrument(skip(password))]
fn login_good(username: &str, password: &str) {
// password excluded
}Always review what data appears in spans.
use tracing::{instrument, subscriber, Level};
use tracing_subscriber::fmt::format::FmtSpan;
fn verify_skip_behavior() {
// Set up a subscriber that shows spans
let subscriber = tracing_subscriber::fmt()
.with_max_level(Level::TRACE)
.with_span_events(FmtSpan::ENTER | FmtSpan::CLOSE)
.finish();
tracing::subscriber::with_default(subscriber, || {
test_function("user", "secret_password", "api_key_123");
});
}
#[instrument(skip(password, api_key))]
fn test_function(username: &str, password: &str, api_key: &str) {
// Verify in output that password and api_key don't appear
}Test instrumentation to ensure sensitive data is excluded.
The #[instrument(skip(...))] attribute provides essential protection for sensitive data:
Skip options:
| Argument | Effect |
|----------|--------|
| skip(a, b) | Exclude parameters a and b from span |
| skip_all | Exclude all parameters |
| skip_all, fields(x = %y) | Skip all, add back specific fields |
Best practices:
skip sensitive parameters like passwords, tokens, keysskip_all when most parameters are sensitiveDebug/Display that mask sensitive types as defense in depthret and err outputs for sensitive datasecrecy crate for types that should never be loggedKey insight: skip excludes parameters from the span entirely—they never appear in logs, traces, or any observability system. This is more reliable than trying to mask data downstream, as it prevents sensitive data from ever entering the tracing pipeline.