Logging
The SDK uses the tracing
crate for structured logging, providing rich context and efficient filtering.
Important: Logs are emitted immediately when generated and are NOT rolled back if a transaction reverts. This means failed transactions will still have their logs recorded, which is useful for debugging or monitoring why transactions failed.
Basic Logging Patterns
// Adapted from the `Bank` module
use tracing::trace;
impl<S: Spec> MyModule<S> {
pub(crate) fn freeze(
&mut self,
token_id: TokenId,
context: &Context<S>,
state: &mut impl TxState<S>,
) -> Result<()> {
// Logging at the start of operation
trace!(freezer = %sender, "Freeze token request");
// Redundant code elided here...
token
.freeze(sender)
.with_context(|| format!("Failed to freeze token_id={}", &token_id))?;
self.tokens.set(&token_id, &token, state)?;
// Logging at the end of operation
trace!(
freezer = %sender,
%token_id,
"Successfully froze tokens"
);
Ok(())
}
}
Using Spans for Context
Spans are like invisible context that gets automatically attached to every log line within their scope. Instead of passing context like batch_id
or user_id
through every function call just so you can log it, you create a span at the top level and all logs within that span automatically include that context.
Think of spans as a way to say "everything that happens from here until the span ends is part of this operation." This is especially useful when debugging - you can filter logs by span fields to see everything that happened during a specific batch process or user request.
use tracing::instrument;
// Example 1: Using the #[instrument] macro (easiest way)
#[instrument(skip(self, state, items))] // skip large/non-Debug types
fn process_batch(&self, batch_id: BatchId, items: Vec<Item>, state: &mut impl TxState<S>) -> Result<()> {
// The #[instrument] macro automatically adds all function parameters (except skipped ones) to the span
// So batch_id is automatically included in all logs within this function
info!(item_count = items.len(), "Starting batch processing");
for (idx, item) in items.iter().enumerate() {
// This log will show: batch_id=123 item_id=456 "Processing item"
trace!(item_index = idx, item_id = %item.id, "Processing item");
self.process_single_item(item, state)?;
}
info!("Batch processing completed");
Ok(())
}
// Example 2: Creating spans manually (when you need more control)
fn process_user_request(&self, user_id: UserId, request: Request) -> Result<()> {
// Create a span with context that will be included in all logs
let span = tracing::span!(
tracing::Level::INFO,
"user_request", // span name
%user_id,
request_type = %request.request_type()
);
// Enter the span - all logs from here will include user_id and request_type
let _enter = span.enter();
debug!("Validating request");
self.validate_request(&request)?;
debug!("Processing request");
self.process(&request)?;
info!("Request completed successfully");
Ok(())
}
Log Levels
error!
- Unrecoverable errors that affect module operationwarn!
- Recoverable issues or unusual conditionsinfo!
- High-level operations (tx processing, module lifecycle)debug!
- Detailed operational data (state changes, intermediate values)trace!
- Very detailed execution flow
Best Practices
-
Structure your logs:
// Good - structured, filterable debug!(user = %address, action = "deposit", amount = %value, "Processing deposit"); // Avoid - unstructured string interpolation debug!("Processing deposit for {} of amount {}", address, value);
-
Include relevant context:
- Transaction/operation IDs
- User addresses (when relevant)
- Amounts and values
- Error details
- State transitions
-
Don't log transaction reverts as errors or warnings: Transaction reverts are expected behavior. Log them at
debug!
level if needed for debugging:if balance < amount { debug!( user = %sender, requested = %amount, available = %balance, "Transfer failed due to insufficient balance" ); return Err(anyhow::anyhow!("Insufficient balance")); }
-
Keep frequently triggered logs at debug or trace level: Any log that gets triggered by every call to your module should use
debug!
ortrace!
to avoid log spam:// Good - routine operations at trace level trace!(method = "transfer", from = %sender, "Processing transfer request"); // Bad - routine operations at info level will spam logs info!("Transfer request received"); // Don't do this for every call
-
Use conditional logging for expensive operations:
#[cfg(feature = "native")] fn debug_state(&self, state: &impl StateAccessor<S>) { if tracing::enabled!(tracing::Level::TRACE) { let total_accounts = self.count_accounts(state); let total_balance = self.calculate_total_balance(state); trace!( %total_accounts, %total_balance, "Module state snapshot" ); } }
Set log levels via environment variables:
RUST_LOG=info,my_module=debug cargo run