Source code for loom.core.config.observability

"""Shared OpenTelemetry configuration objects used by Loom runtimes."""

from __future__ import annotations

import warnings
from typing import Any
from urllib.parse import urlparse

from loom.core.model import LoomFrozenStruct

_LOCAL_HOSTS = {"127.0.0.1", "localhost", "::1"}

SAMPLERS: frozenset[str] = frozenset(
    {
        "always_on",
        "always_off",
        "traceidratio",
        "parentbased_always_on",
        "parentbased_always_off",
        "parentbased_traceidratio",
    }
)
"""Sampler names accepted by :attr:`OtelConfig.sampler`."""


def _is_local_endpoint(endpoint: str) -> bool:
    return (urlparse(endpoint).hostname or "") in _LOCAL_HOSTS


[docs] class OtelConfig(LoomFrozenStruct, frozen=True): """OpenTelemetry SDK/exporter configuration. Args: service_name: Resource attribute ``service.name``. tracer_name: Tracer instrumentation name. tracer_version: Optional tracer instrumentation version. protocol: OTLP protocol (``http/protobuf`` or ``grpc``). endpoint: OTLP endpoint URI. When empty, uses global OTel runtime defaults. insecure: Exporter transport mode when supported by protocol/exporter. headers: Exporter request headers (vendor auth/tags). resource_attributes: Additional OTel resource attributes. span_attributes: Static span attributes added to all spans emitted by this observer. exporter_kwargs: Extra keyword args passed through to OTLP exporter. span_processor_kwargs: Extra keyword args passed through to BatchSpanProcessor. adopt_host_id_generator: Installs Loom's trace-id generator on the already-installed ``TracerProvider`` so streaming messages get end-to-end traces. Host spans are unaffected — the generator delegates to the default random generator whenever no Loom trace id is active. Only meaningful when ``endpoint`` is empty; Loom's own provider always gets the generator. sampler: Sampler name, one of :data:`SAMPLERS`. Applied to Loom's own provider only: with an empty ``endpoint`` the host sampler decides. sampler_ratio: Sampled fraction for the ratio-based samplers. Because the OTEL trace id *is* the message trace id, the decision is stable from ingestion to death and across services, so a sampled message yields a complete trace and an unsampled one yields none. max_span_links: Upper bound on the links a batch span carries. When the bound is hit the span is marked with ``loom.links_truncated``. """ service_name: str = "loom" tracer_name: str = "loom" tracer_version: str = "" protocol: str = "http/protobuf" endpoint: str = "" insecure: bool = False headers: dict[str, str] = {} resource_attributes: dict[str, str] = {} span_attributes: dict[str, str] = {} exporter_kwargs: dict[str, Any] = {} span_processor_kwargs: dict[str, Any] = {} adopt_host_id_generator: bool = False sampler: str = "parentbased_always_on" sampler_ratio: float = 1.0 max_span_links: int = 128
[docs] def validate(self) -> None: """Validate the transport protocol, the sampler, and the link bound. Raises: ValueError: If protocol is unsupported. ValueError: If the sampler name is unknown, the sampled ratio is outside ``[0.0, 1.0]``, or the link bound is not positive. """ if self.protocol not in {"http/protobuf", "grpc"}: raise ValueError( "observability.otel_config.protocol must be either 'http/protobuf' or 'grpc'." ) if self.sampler not in SAMPLERS: raise ValueError( "observability.otel_config.sampler must be one of " f"{sorted(SAMPLERS)}, got {self.sampler!r}." ) if not 0.0 <= self.sampler_ratio <= 1.0: raise ValueError( "observability.otel_config.sampler_ratio must be within [0.0, 1.0], " f"got {self.sampler_ratio!r}." ) if self.max_span_links < 1: raise ValueError( "observability.otel_config.max_span_links must be >= 1, " f"got {self.max_span_links!r}." ) if self.insecure and self.endpoint and not _is_local_endpoint(self.endpoint): warnings.warn( f"OtelConfig.insecure=True on non-local endpoint {self.endpoint!r}." " Set insecure=False for production deployments.", UserWarning, stacklevel=2, )
__all__ = ["SAMPLERS", "OtelConfig"]