Skip to content

windlass.providers.observability.multi

multi

Fan-out tracing — send one trace to several backends at once.

Teams rarely have exactly one place they want traces to land. A platform team standardises on LangSmith while a product team already reads Langfuse; during a migration you want both; while debugging you want the console too. Without this you would have to pick one, or write the fan-out by hand in every application.

::

rag = Windlass.rag().observe("multi", backends=["langfuse", "langsmith"])

agent = Windlass.agent().observe(
    "multi", backends=["console", Windlass.tracer("langfuse")]
)

Backends are named exactly as they are in the registry, or passed as live :class:~windlass.interfaces.tracer.Tracer instances when they need their own configuration.

A tracer is never allowed to break the application it observes, and that guarantee gets harder with several backends, not easier: one misconfigured exporter must not take down the others or the run. Every call into a backend is therefore isolated — a backend that raises is logged once and skipped for the rest of that call, never propagated to the caller.

Example

from windlass import Windlass tracer = Windlass.tracer("multi", backends=["memory", "memory"]) with tracer.span("work", kind="chain"): ... pass tuple(len(b.spans) for b in tracer.backends) (1, 1)

MultiTracer

MultiTracer(
    *,
    backends: Sequence[Any] = (),
    project: str | None = None,
    tags: tuple[str, ...] = (),
    enabled: bool = True,
    **config: Any
)

Bases: Tracer

A tracer that forwards every span to several other tracers.

Parameters:

Name Type Description Default
backends Sequence[Any]

Registry names ("langfuse", "console", ...) or live :class:~windlass.interfaces.tracer.Tracer instances.

()
project str | None

Project/session name passed to any backend built by name.

None
tags tuple[str, ...]

Tags passed to any backend built by name.

()
enabled bool

Master switch.

True
**config Any

Forwarded to :class:~windlass.interfaces.tracer.Tracer.

{}

Attributes:

Name Type Description
backends list[Tracer]

The live backend tracers, in call order.

Raises:

Type Description
ConfigurationError

When a named backend cannot be constructed. A backend you asked for by name and which cannot be built is a configuration mistake worth failing on — unlike a runtime export failure, which is isolated and logged.

Example

from windlass import Windlass tracer = Windlass.tracer("multi", backends=["memory"]) len(tracer.backends) 1

Source code in src\windlass\providers\observability\multi.py
def __init__(
    self,
    *,
    backends: Sequence[Any] = (),
    project: str | None = None,
    tags: tuple[str, ...] = (),
    enabled: bool = True,
    **config: Any,
) -> None:
    super().__init__(enabled=enabled, project=project, tags=tags, **config)
    from windlass.core.registry import REGISTRY

    resolved: list[Tracer] = []
    for backend in backends:
        if isinstance(backend, str):
            options: dict[str, Any] = {}
            if project is not None:
                options["project"] = project
            if tags:
                options["tags"] = tags
            resolved.append(REGISTRY.create("tracer", backend, **options))
        else:
            resolved.append(backend)
    self.backends: list[Tracer] = resolved
    self._broken: set[int] = set()

start_span

start_span(span: Span) -> None

Open the span on every backend.

Source code in src\windlass\providers\observability\multi.py
def start_span(self, span: Span) -> None:
    """Open the span on every backend."""
    self._each("start_span", span)

end_span

end_span(span: Span) -> None

Close the span on every backend.

Source code in src\windlass\providers\observability\multi.py
def end_span(self, span: Span) -> None:
    """Close the span on every backend."""
    self._each("end_span", span)

flush

flush() -> None

Flush every backend.

Source code in src\windlass\providers\observability\multi.py
def flush(self) -> None:
    """Flush every backend."""
    self._each("flush")

aclose async

aclose() -> None

Close every backend, ignoring individual failures.

Source code in src\windlass\providers\observability\multi.py
async def aclose(self) -> None:
    """Close every backend, ignoring individual failures."""
    for backend in self.backends:
        close = getattr(backend, "aclose", None)
        if close is None:
            continue
        try:
            await close()
        except Exception as exc:  # pragma: no cover - shutdown best effort
            self._log.debug("Closing tracing backend failed: %s", exc)

native

native() -> list[Any]

Return the underlying client of each backend (Level 3 access).

Source code in src\windlass\providers\observability\multi.py
def native(self) -> list[Any]:
    """Return the underlying client of each backend (Level 3 access)."""
    return [b.native() for b in self.backends]

describe

describe() -> dict[str, Any]

Return a JSON-safe summary naming every backend.

Source code in src\windlass\providers\observability\multi.py
def describe(self) -> dict[str, Any]:
    """Return a JSON-safe summary naming every backend."""
    info = super().describe()
    info["backends"] = [getattr(b, "name", type(b).__name__) for b in self.backends]
    return info