How Macro-Inc/Macro Handles Logging: Structured Observability with Tracing
Macro-Inc/Macro leverages the tracing crate for structured, hierarchical logging with configurable subscribers, environment-driven filtering via RUST_LOG, and optional OpenTelemetry integration for distributed tracing.
The macro-inc/macro codebase demonstrates production-grade logging patterns using Rust's tracing ecosystem rather than simple text output. This architecture treats log entries as structured events within execution contexts called spans, enabling detailed observability across asynchronous service boundaries. Understanding how macro handles logging provides a blueprint for implementing configurable, high-performance observability in Rust applications.
The Tracing Foundation
The entire logging infrastructure rests on the tracing crate, which provides structured, context-aware logging. Unlike traditional log crates that output flat text lines, tracing organizes logs into spans representing units of work with defined parent-child relationships.
This hierarchical approach means that when a function logs an event, it automatically carries context from parent operations. The codebase extensively uses #[tracing::instrument] attributes to automatically create spans around function entry and exit, eliminating boilerplate while ensuring consistent instrumentation throughout the application.
Subscriber Initialization in Entry Points
Logging initialization occurs at application startup in binary entry points. The repository demonstrates this pattern in tooling/notification_sandbox/src/main.rs at line 63 and tooling/native_app_server/src/main.rs at line 76.
These entry points construct a tracing_subscriber registry that drives the logging pipeline. The typical initialization pattern configures a layered subscriber combining filtering with formatting:
use tracing_subscriber::{fmt, EnvFilter};
fn init_logging() {
// Use the RUST_LOG env var, default to "info"
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
// Plain‑text formatter with thread names and targets
let fmt_layer = fmt::layer()
.with_target(true)
.with_thread_names(true);
// Build the subscriber and install it globally
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.init();
}
The registry pattern allows composable layering, where each layer handles a specific concern such as filtering, formatting, or forwarding to external collectors.
Environment-Based Filtering
Log verbosity control uses EnvFilter from tracing_subscriber, which reads the RUST_LOG environment variable at runtime. When RUST_LOG is unset, the system falls back to a sensible default of info level logging.
This approach enables granular control without code changes. Developers can specify module-specific levels using syntax like RUST_LOG=debug,macro::internal=error to see debug output globally while restricting noisy internal modules to errors only.
Output Formatting: JSON vs. Plain Text
The codebase supports both human-readable and machine-parseable output formats. By default, development environments use the plain-text formatter shown above, which includes target modules and thread names for debugging.
For production deployments, crates/macro_entrypoint/src/lib.rs demonstrates switching to JSON formatting:
use tracing_subscriber::{fmt, EnvFilter};
let json_fmt = fmt::format::Format::default()
.json()
.with_current_span(true);
let json_layer = fmt::layer()
.event_format(json_fmt);
tracing_subscriber::registry()
.with(filter)
.with(json_layer)
.init();
JSON output integrates with log aggregation systems, preserving structured field data that plain text would flatten into strings.
OpenTelemetry Integration for Distributed Tracing
Beyond local logging, the worker-rs-otel crate provides distributed tracing capabilities through OpenTelemetry. Located in crates/worker-rs-otel/src/layer/mod.rs, this integration adds an OtelLayer to the subscriber stack.
This layer forwards span data to configured OpenTelemetry collectors while maintaining local log output, creating a unified observability pipeline. The approach allows correlation of logs across service boundaries without sacrificing local debugging capabilities.
Instrumentation Patterns and Field Enrichment
Business logic instrumentation relies heavily on the #[tracing::instrument] attribute macro. This macro automatically creates a span when a function is called, recording arguments and function names.
The S3 client in services/static_file_service/src/service/s3/client.rs demonstrates heavy instrumentation:
#[tracing::instrument(skip(self), err)]
pub async fn get_object(&self, key: &str) -> Result<Vec<u8>> {
// Function body automatically wrapped in a span
tracing::info!("Fetching object {}", key);
Ok(vec![])
}
The skip parameter prevents large or sensitive arguments from being recorded, while err automatically records error information when functions return Result::Err.
For additional context, developers use the fields attribute to inject custom data:
#[tracing::instrument(skip(ctx), fields(request_id = %req_id))]
pub async fn handle_request(ctx: &Context, req_id: Uuid) -> Result<()> {
// All logs inside this function automatically carry `request_id`
tracing::debug!("Started handling request");
Ok(())
}
The search processing worker in services/search_processing_service/src/process/worker.rs uses #[tracing::instrument(skip(ctx))] to instrument long-running background tasks without capturing heavy context objects.
Summary
- Macro-Inc/Macro builds logging on the
tracingcrate for structured, span-based observability. - Subscriber initialization happens in binary entry points like
tooling/native_app_server/src/main.rs, configuring layers for filtering and formatting. EnvFilterdrives log levels through theRUST_LOGenvironment variable, defaulting toinfowhen unset.- Output formats switch between human-readable plain text (development) and JSON (production) via
crates/macro_entrypoint/src/lib.rs. - The
worker-rs-otelcrate adds OpenTelemetry distributed tracing through layers defined incrates/worker-rs-otel/src/layer/mod.rs. #[tracing::instrument]automates span creation around function boundaries, withskipandfieldsparameters controlling captured data.
Frequently Asked Questions
How do I change the log level in Macro?
Set the RUST_LOG environment variable before starting the application. Use RUST_LOG=debug for verbose output or target specific modules with RUST_LOG=macro_entrypoint=debug,error to see debug logs only from the entry point crate while keeping everything else at error level.
What is the difference between spans and events in this logging system?
Spans represent periods of time with a beginning and end, such as a function execution or database query. Events are discrete points in time, like log messages. In tracing, events occur within the context of the current span, automatically inheriting contextual fields like request_id without explicit passing.
How do I add custom fields to logs without modifying every log statement?
Use the fields parameter in #[tracing::instrument(fields(key = value))]. All logging macros (tracing::info, tracing::debug, etc.) executed within that function automatically include these fields in their output. You can also dynamically update spans using Span::record for values only known at runtime.
Is OpenTelemetry integration required to run the application?
No. The OpenTelemetry layer in crates/worker-rs-otel/src/layer/mod.rs is optional. The application runs fine with just the standard fmt layer for console output. OpenTelemetry is only needed when you want to forward traces to external collectors for distributed tracing across multiple services.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →