# How Macro-Inc/Macro Handles Logging: Structured Observability with Tracing

> Discover how macro-inc/macro handles logging with structured observability and tracing. Learn about its configurable subscribers, environment-driven filtering, and OpenTelemetry integration.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: how-to-guide
- Published: 2026-08-15

---

**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`](https://github.com/macro-inc/macro/blob/main/tooling/notification_sandbox/src/main.rs) at line 63 and [`tooling/native_app_server/src/main.rs`](https://github.com/macro-inc/macro/blob/main/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:

```rust
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`](https://github.com/macro-inc/macro/blob/main/crates/macro_entrypoint/src/lib.rs) demonstrates switching to JSON formatting:

```rust
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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/services/static_file_service/src/service/s3/client.rs) demonstrates heavy instrumentation:

```rust
#[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:

```rust
#[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`](https://github.com/macro-inc/macro/blob/main/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 `tracing` crate for structured, span-based observability.
- Subscriber initialization happens in binary entry points like [`tooling/native_app_server/src/main.rs`](https://github.com/macro-inc/macro/blob/main/tooling/native_app_server/src/main.rs), configuring layers for filtering and formatting.
- **`EnvFilter`** drives log levels through the `RUST_LOG` environment variable, defaulting to `info` when unset.
- Output formats switch between human-readable plain text (development) and JSON (production) via [`crates/macro_entrypoint/src/lib.rs`](https://github.com/macro-inc/macro/blob/main/crates/macro_entrypoint/src/lib.rs).
- The **`worker-rs-otel`** crate adds OpenTelemetry distributed tracing through layers defined in [`crates/worker-rs-otel/src/layer/mod.rs`](https://github.com/macro-inc/macro/blob/main/crates/worker-rs-otel/src/layer/mod.rs).
- **`#[tracing::instrument]`** automates span creation around function boundaries, with `skip` and `fields` parameters 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`](https://github.com/macro-inc/macro/blob/main/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.