Expand description
§log-args: Procedural Macro for Logging Function Arguments with Async Support
This crate provides the #[params]
attribute macro to automatically log function arguments using the tracing
crate,
with special support for handling ownership in asynchronous contexts.
§Features
- Log all function arguments automatically or select specific ones
- Log nested fields of struct arguments (e.g.,
user.id
) - Add custom key-value pairs to log output
- Compatible with both synchronous and asynchronous functions
- Special
clone_upfront
option for handling ownership in async move blocks and spawned tasks - No tracing spans or span-related features used - just simple structured logging
§Basic Usage
use log_args::params;
use tracing::info;
#[derive(Debug)]
struct User { id: u32, name: String }
// Log all arguments
#[params]
fn process_user(user: User, count: usize) {
info!("Processing user data");
// Logs: INFO process_user: Processing user data user=User { id: 42, name: "Alice" } count=5
}
// Log only specific fields
#[params(fields(user.id, count))]
fn validate_user(user: User, count: usize) {
info!("Validating user");
// Logs: INFO validate_user: Validating user user.id=42 count=5
}
// Add custom values
#[params(custom(service = "auth", version = "1.0"))]
fn authenticate(user: User) {
info!("Authentication attempt");
// Logs: INFO authenticate: Authentication attempt user=User { id: 42, name: "Alice" } service="auth" version="1.0"
}
§Advanced: Async Support with clone_upfront
When working with asynchronous code, especially when moving values into async move
blocks or
tokio::spawn
, you might encounter ownership issues. The clone_upfront
option addresses this
by ensuring fields can be safely used throughout your async function:
use log_args::params;
use tracing::info;
#[derive(Debug, Clone)]
struct Client { id: String, name: String }
#[params(clone_upfront, fields(client.id, client.name))]
async fn process_client(client: Client) {
info!("Starting client processing");
// Move client into an async block
let task = tokio::spawn(async move {
// Use client here without ownership issues
// ...
client
});
// Logs still work even though client was moved
// because values were cloned upfront
info!("Waiting for client processing to complete");
}
§Macro Attributes
-
fields(...)
: Log only the specified arguments or fields (e.g.,fields(user.id, count)
). This is useful for reducing log verbosity or accessing nested fields of struct arguments. -
custom(...)
: Add custom key-value pairs to the log output (e.g.,custom(service = "auth")
). This allows adding static context to your logs that isn’t directly from function arguments. -
clone_upfront
: Clone fields upfront to avoid ownership issues in async contexts. This is particularly useful when working withasync move
blocks or spawned tasks where the original variables are moved.
§How It Works
The #[params]
macro redefines the tracing macros (info!
, warn!
, error!
, debug!
, trace!
)
within the function body to automatically include the specified function arguments or fields in the
log output. For clone_upfront
, it generates code that safely handles ownership by cloning values
as needed to ensure they’re available throughout the function execution.
When using with async functions, especially those containing tokio::spawn
with async move
blocks,
be careful about ownership and lifetimes. There are three patterns for handling this:
§Pattern 1: Using with owned parameters (default behavior)
#[params(fields(session.user_id, session.session_id))]
async fn process_session(session: Session) {
info!("Processing session"); // Fields are cloned here
// Do some processing
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
info!("Session processing completed"); // Fields are cloned here too
}
§Pattern 2: Using the clone_upfront
option (for async functions)
This pattern clones all fields at the beginning of the function, which helps with logging
in async functions. Note that for tokio::spawn
with async move
blocks, you’ll still need
to clone the fields again before the async block:
#[params(clone_upfront, fields(self.client_id, self.company_id))]
async fn run(self, socket: WebSocket) {
info!("Starting connection"); // Uses cloned fields
// Clone variables are created at the beginning of the function
// so they're available for all tracing macros in the main function body
// For tokio::spawn, you need to clone again for the async block
let task_client_id = self.client_id.clone();
let task_company_id = self.company_id.clone();
let task = tokio::spawn(async move {
// Use the task-specific clones inside the async block
info!(client_id = ?task_client_id, company_id = ?task_company_id, "Worker task started");
});
info!("Connection handler completed"); // Uses the original cloned fields
}
§Pattern 3: Manual logging with references and async blocks
When you need more control, you can manually clone and log fields:
async fn handle_connection(client_id: String, company_id: String) {
// Log manually at the beginning
info!(client_id = ?client_id, company_id = ?company_id, "Starting connection");
// Clone what we need for the async block
let task_client_id = client_id.clone();
// Spawn the async task with its own cloned data
let task = tokio::spawn(async move {
info!(client_id = ?task_client_id, "Worker task started");
});
// This log can still use the original client_id
info!(client_id = ?client_id, "Connection handler completed");
}
See the examples directory for more detailed usage patterns.
§Limitations
- Only works with the
tracing
crate macros (info!
,debug!
,warn!
,error!
,trace!
). - Does not support span creation or level selection via macro input.
- All arguments must implement
Debug
for structured logging. - For async code with
tokio::spawn
, use theclone_upfront
option to avoid ownership issues. - Generates warnings about unused macro definitions (these are expected and can be ignored).
See the examples directory for more detailed usage patterns.
Attribute Macros§
- params
- A procedural macro attribute for automatically logging function arguments.