What is the difference between tracing::info! and info_span! for creating log entries vs trace scopes?
tracing::info! creates a one-time event that records a log entry at a specific point in time, while info_span! creates a span that represents a duration of time and establishes a context for all events that occur within it. Events are instantaneous recordings of data, whereas spans are scoped contexts that can be entered and exited, carrying contextual information that applies to all nested operations and events.
Basic Event Creation with info!
use tracing::{info, Level};
fn basic_event() {
// info! creates an event: a single point-in-time log record
info!("Application started");
// Events record data at the moment they're called
let user_id = 12345;
info!(user_id, "User logged in");
// With structured fields
info!(
user_id = %user_id,
action = "login",
"User action recorded"
);
// Events are similar to traditional log statements
// They record: timestamp, level, message, fields
// Then they're done - no duration, no context
}info! creates events that are recorded once and complete immediately.
Basic Span Creation with info_span!
use tracing::{info_span, Span};
fn basic_span() {
// info_span! creates a span: a duration with context
let span = info_span!("http_request", method = "GET", path = "/users");
// The span exists but doesn't record anything yet
// It needs to be entered to become active
// Enter the span
let _enter = span.enter();
// Now this span is the current context
// Any events inside will have this span's context
info!("Processing request"); // This event is inside the span
// Exiting the scope exits the span
// The span's duration is: enter time to exit time
}info_span! creates a span that represents a duration of time with contextual data.
Events vs Spans Conceptual Model
use tracing::{info, info_span, Level};
fn events_vs_spans() {
// EVENTS: Point-in-time recordings
// - Instantaneous
// - Record data once
// - Similar to traditional log statements
// - No duration
info!("This is an event"); // Recorded immediately, done
// SPANS: Duration-based contexts
// - Have a start and end
// - Establish context for contained operations
// - Can be entered and exited multiple times
// - Carry fields that apply to all nested operations
let span = info_span!("database_query", query_id = 123);
let _enter = span.enter();
info!("Query started"); // Event inside span context
info!("Query completed"); // Event inside span context
// The span provides context for both events
// When analyzing: both events belong to "database_query" span
}Events are points; spans are durations with context.
Span Context and Nested Events
use tracing::{info, info_span, span, Level};
fn span_context() {
// Spans provide context for all events within them
let request_span = info_span!("http_request", request_id = %12345);
let _enter = request_span.enter();
// All events here have request_id = 12345 in their context
info!("Request received");
{
// Nested span
let db_span = info_span!("database_query", table = "users");
let _db_enter = db_span.enter();
info!("Query executed"); // Context: both http_request and database_query
// This event belongs to both spans:
// - http_request (parent)
// - database_query (current)
}
info!("Sending response"); // Back to just http_request context
}
// When analyzing traces:
// - http_request { request_id: 12345 }
// - database_query { table: "users" }
// - "Query executed" event (has both contexts)
// - "Sending response" event (has http_request context)Spans create a hierarchical context that events inherit.
Levels for Events and Spans
use tracing::{info, debug, trace, warn, error, info_span, debug_span, trace_span, Level};
fn levels() {
// Events have levels (similar to log levels)
trace!("Very detailed information");
debug!("Debug information");
info!("General information");
warn!("Warning message");
error!("Error message");
// Spans also have levels
// The level indicates the span's importance
let trace_span = trace_span!("detailed_operation"); // TRACE level
let debug_span = debug_span!("debug_operation"); // DEBUG level
let info_span = info_span!("important_operation"); // INFO level
// Level filtering applies to both:
// - If level is filtered out, events aren't recorded
// - If span level is filtered out, span isn't recorded
// - Events inside filtered spans may still be recorded
// (depending on subscriber implementation)
}Both events and spans use the same level hierarchy for filtering.
Span Lifecycle
use tracing::{info, info_span, Span};
fn span_lifecycle() {
// Create span (not yet active)
let span = info_span!("operation", id = 1);
// Span exists but is idle
// No duration tracking yet
// Enter span (makes it current)
let enter1 = span.enter();
info!("First entry"); // Recorded inside span
// Exit span (by dropping enter guard)
drop(enter1);
// Span is idle again, but still exists
// Can enter again
let enter2 = span.enter();
info!("Second entry"); // Recorded inside same span
drop(enter2);
// Span can be cloned and entered from multiple threads
let span2 = span.clone();
// Span is finalized when all references are dropped
// Subscriber receives: span created, entered, exited, closed
}Spans have a lifecycle: created â entered â exited â closed.
Field Inheritance
use tracing::{info, info_span, debug_span};
fn field_inheritance() {
// Parent span with fields
let parent = info_span!("request", request_id = 123, user_id = 456);
let _parent_enter = parent.enter();
// Child span
let child = debug_span!("database_query", query = "SELECT * FROM users");
let _child_enter = child.enter();
info!("Query result returned");
// This event has access to all fields:
// - request_id = 123 (from parent)
// - user_id = 456 (from parent)
// - query = "SELECT * FROM users" (from current)
// Subscribers can access all parent fields when processing events
}Events inherit fields from all active spans in their context.
When to Use Events
use tracing::{info, info_span, warn, error, Level};
fn when_to_use_events() {
// Use events for:
// 1. Point-in-time occurrences
info!("Application started");
info!("Configuration loaded");
// 2. One-off observations
let connection_count = 42;
info!(connection_count, "Current connection count");
// 3. Error and warning messages
error!(error_code = 500, "Internal server error");
warn!("Cache miss rate is high");
// 4. Metric recordings
let latency_ms = 150;
info!(latency_ms, "Request latency");
// 5. Debug information
info!(?result, "Function returned");
// Events are appropriate when you just need to record something
// without establishing a context for other operations
}Use events for single-point observations without duration context.
When to Use Spans
use tracing::{info, info_span, debug_span, Level};
fn when_to_use_spans() {
// Use spans for:
// 1. Operations with duration
let span = info_span!("http_request", method = "GET", path = "/api/users");
let _enter = span.enter();
// All events during this operation share context
info!("Request received");
// ... processing ...
info!("Response sent");
// 2. Establishing context for nested operations
fn process_request() {
let span = info_span!("process_request", request_id = 123);
let _enter = span.enter();
validate_input(); // Events inside know they're in process_request
query_database(); // Events inside know they're in process_request
format_response(); // Events inside know they're in process_request
}
// 3. Tracing distributed operations
let span = info_span!("database_transaction", tx_id = %generate_id());
let _enter = span.enter();
// 4. Performance profiling
let span = info_span!("expensive_operation");
let _enter = span.enter();
// Span duration = time spent in expensive operation
}
fn validate_input() {
debug!("Validating input");
}
fn query_database() {
let span = debug_span!("database_query");
let _enter = span.enter();
debug!("Executing query");
}
fn format_response() {
debug!("Formatting response");
}Use spans when context should carry across multiple operations.
Structured Fields Comparison
use tracing::{info, info_span, Level};
fn structured_fields() {
// Events: fields are recorded once with the event
info!(
user_id = 12345,
action = "login",
ip_address = "192.168.1.1",
"User logged in"
);
// All fields are attached to this single event
// Spans: fields establish context for duration
let span = info_span!(
"http_request",
method = "GET",
path = "/api/users",
user_id = 12345 // This user_id applies to ALL events in span
);
let _enter = span.enter();
// Events inside can omit user_id - it's in the span
info!("Request started");
info!("Processing parameters");
info!("Sending response");
// All three events have user_id = 12345 in their context
// Without repeating the field in each event
}Span fields provide context for multiple events; event fields are one-time.
Span Creation Patterns
use tracing::{info, info_span, Level, Span};
fn span_patterns() {
// Pattern 1: Enter immediately
{
let _enter = info_span!("operation").entered();
info!("Inside operation");
}
// Pattern 2: Create and enter separately
let span = info_span!("another_operation", id = 42);
{
let _enter = span.enter();
info!("Inside another operation");
}
// Can enter again later
{
let _enter = span.enter();
info!("Inside same operation again");
}
// Pattern 3: Span with dynamic fields
let user_id = get_user_id();
let span = info_span!("user_operation", user_id = %user_id);
let _enter = span.enter();
// Pattern 4: Conditional span level
let level = if is_important { Level::INFO } else { Level::DEBUG };
let span = Span::new(level, "conditional_operation",
tracing::field::display("value"));
let _enter = span.enter();
}
fn get_user_id() -> u64 { 42 }Spans offer flexible creation and entry patterns.
Practical Example: HTTP Handler
use tracing::{info, info_span, debug, error, Level};
// Full example showing events and spans together
struct HttpRequest {
method: String,
path: String,
user_id: Option<u64>,
}
async fn handle_request(req: HttpRequest) -> Result<String, String> {
// Create span for entire request
let span = info_span!(
"http_request",
method = %req.method,
path = %req.path,
user_id = tracing::field::Empty, // May be set later
);
let _enter = span.enter();
// Event: request received
info!("Request received");
// Set user_id if available
if let Some(uid) = req.user_id {
span.record("user_id", &uid);
}
// Validate
validate_request(&req)?;
debug!("Request validated"); // Event inside span
// Process
let result = process_request(&req).await;
// Event: request completed
info!("Request completed successfully");
Ok(result)
}
async fn process_request(req: &HttpRequest) -> String {
// Child span for processing
let span = info_span!("process_request");
let _enter = span.enter();
// Events inside have both parent and child span context
debug!("Processing request data");
// Database query (another nested span)
let db_result = query_database().await;
debug!("Database query completed");
db_result
}
async fn query_database() -> String {
let span = debug_span!("database_query", table = "users");
let _enter = span.enter();
debug!("Executing SQL query"); // Event
"result".to_string()
}
fn validate_request(req: &HttpRequest) -> Result<(), String> {
if req.method.is_empty() {
error!("Invalid method"); // Event
return Err("Invalid method".to_string());
}
Ok(())
}A complete HTTP handler shows how spans create context hierarchy.
Performance Considerations
use tracing::{info, info_span, Level};
fn performance() {
// Events: minimal overhead
// - Record and done
// - No state to track
for i in 0..1000 {
info!(i, "Processing item"); // 1000 events
}
// Spans: more overhead
// - Track entry/exit
// - Maintain context stack
// - But fields are recorded once
for i in 0..1000 {
let span = info_span!("process_item", item_id = i);
let _enter = span.enter();
info!("Processing"); // Has item_id in context
// vs recording item_id in every event:
// info!(item_id = i, "Processing"); // Repeats field
}
// Best practice: Use spans when context saves repetition
// Use events for one-off recordings
}Spans have overhead but can reduce repetition in events.
Subscriber Behavior
use tracing::{info, info_span, Level};
// Different subscribers handle events and spans differently
fn subscriber_behavior() {
// Subscriber receives:
// For events:
// - event() callback with: metadata, fields, parent span
// For spans:
// - new_span() when created
// - enter() when entered
// - exit() when exited
// - close() when all references dropped
// Example subscriber behavior:
// fmt subscriber (pretty printing):
// - Events print as log lines
// - Spans print "enter" and "exit" markers
// json subscriber:
// - Each event is a JSON object with span context
// - Spans are tracked separately
// jaeger/tracing subscriber:
// - Spans become trace spans
// - Events become logs within spans
// The key insight:
// - Events are always recorded (if not filtered)
// - Spans may be sampled/filtered based on level
}Subscribers receive different callbacks for events vs spans.
Memory and References
use tracing::{info, info_span, Span};
fn memory_management() {
// Events have no persistent state
info!("This event is recorded and forgotten");
// No memory retained after recording
// Spans have state
let span = info_span!("operation");
// Span tracks:
// - Creation time
// - Field values
// - Entry count (how many times entered)
// - Reference count
// Multiple references to same span
let span2 = span.clone();
// Span is closed when all references dropped
drop(span);
// span2 still holds reference - span not closed yet
drop(span2);
// Now span is closed
}Events are transient; spans persist until all references are dropped.
Synthesis
Core distinction:
// Event: point-in-time recording
info!("User logged in", user_id = 123);
// Records once, done
// No context for subsequent events
// Span: duration-based context
let span = info_span!("http_request", request_id = 123);
let _enter = span.enter();
info!("Processing request"); // Has request_id in context
info!("Sending response"); // Has request_id in context
// Both events belong to the spanComparison:
| Aspect | info! Event |
info_span! Span |
|---|---|---|
| Duration | Instantaneous | Start to end |
| Context | None (standalone) | Provides context |
| Fields | Recorded once | Available to nested events |
| State | None | Tracks entry/exit |
| Use case | One-off observations | Operations with duration |
| Hierarchy | None | Parent-child relationships |
| Memory | Transient | Persistent until closed |
When to use each:
// Use info! for:
// - Single log messages
// - Error/warning recordings
// - Metric observations
// - Debug output
// Use info_span! for:
// - HTTP requests
// - Database transactions
// - Function calls with duration
// - Any operation that has child operations
// - When context should carry to nested calls
// Typical pattern: span around operation, events inside
async fn handle_user(user_id: u64) {
let span = info_span!("handle_user", user_id);
let _enter = span.enter();
info!("Starting user handling"); // Event
validate_user(user_id).await; // May have its own spans
process_user(user_id).await; // May have its own spans
info!("Completed user handling"); // Event
}Key insight: The fundamental difference is that info! creates eventsâpoint-in-time log entries similar to traditional loggingâwhile info_span! creates a scope that represents a duration of time and provides context for all operations within it. Events answer "what happened at this moment?" while spans answer "what happened during this operation, and what was the context?" Use events for single observations that don't need to establish context for subsequent code, and use spans when you're tracing an operation that has internal steps, child operations, or multiple related events that should share context. The span's fields are automatically inherited by all events within its scope, eliminating repetitive field declarations and creating a trace hierarchy that distributed tracing systems can visualize as parent-child relationships.
