Skip to content

Tracing

The tracing module provides unified instrumentation inspired by Rust's tracing crate. A single @instrument decorator creates OTel spans and enriches log records with structured context automatically.

Quick Start

from grelmicro.log import configure
from grelmicro.trace import instrument, span, add_context
import logging

configure()
logger = logging.getLogger(__name__)

@instrument
async def process_order(order_id: str, user_id: str):
    logger.info("started")
    # {"time":...,"level":"INFO","msg":"started","logger":...,"order_id":"ORD-1","user_id":"USR-1"}

    add_context(payment_status="pending")
    logger.info("payment initiated")
    # {"time":...,"level":"INFO","msg":"payment initiated","logger":...,"order_id":"ORD-1","user_id":"USR-1","payment_status":"pending"}

    with span("db_query", table="orders"):
        logger.info("querying")
        # {"time":...,"level":"INFO","msg":"querying","logger":...,"order_id":"ORD-1","user_id":"USR-1","payment_status":"pending","table":"orders"}

    logger.info("done")
    # table removed (span exited), payment_status still present

API

@instrument

Decorator that captures function arguments as structured context. Works with sync and async functions.

# Bare decorator: captures all arguments
@instrument
async def process(order_id: str, user_id: str): ...

# Skip sensitive arguments
@instrument(skip={"password", "token"})
async def login(username: str, password: str): ...

# Skip all arguments
@instrument(skip_all=True)
async def bulk_process(payload: bytes): ...

# Custom span name
@instrument(name="db.query")
async def fetch_user(user_id: str): ...

span()

Context manager for mid-function instrumentation. Creates a nested context that is automatically removed on exit.

@instrument
async def handle_request(request_id: str):
    logger.info("received")  # has request_id

    with span("auth", method="jwt"):
        logger.info("authenticating")  # has request_id + method

    with span("db", table="users"):
        logger.info("querying")  # has request_id + table

    logger.info("done")  # back to request_id only

add_context()

Add fields to the current context. Updates both log records and the active OTel span (if tracing is configured).

@instrument
async def process(order_id: str):
    result = charge()
    add_context(payment_id=result.id, status=result.status)
    logger.info("charged")  # includes payment_id and status

Configuration

The tracing context enriches log records regardless of how logging is configured. No additional configuration is needed.

When OpenTelemetry is installed, @instrument and span() also create OTel spans and add the same fields as span attributes. A single decorator produces both structured logs and distributed traces.

Install

OpenTelemetry integration needs the opentelemetry extra: pip install "grelmicro[opentelemetry]". See the installation guide for uv and poetry.

# Logging only (no OTel dependency needed)
configure()

# Logging + OTel: install opentelemetry and configure your exporter separately.
# @instrument and span() will automatically create OTel spans when opentelemetry
# is installed and a TracerProvider is configured.

Works With All Backends

The tracing context is injected into all three logging backends (stdlib, loguru, structlog). Use whichever logger you prefer:

# stdlib
import logging
logger = logging.getLogger(__name__)
logger.info("message", extra={"key": "value"})

# loguru
from loguru import logger
logger.info("message", key="value")

# structlog
import structlog
log = structlog.get_logger()
log.info("message", key="value")

All produce the same JSON output with tracing context included.