lexora / apps /api /app /core /observability.py
Abdr007's picture
Lexora — deployed tree
3fc8e60
Raw
History Blame Contribute Delete
3.85 kB
"""Langfuse tracing, wrapped so it can never take the service down.
Observability is the first thing to fail in production — an expired key, a network
partition, a provider outage — and the last thing that should be allowed to fail a user's
request because of it. Every entry point here is therefore total: if Langfuse is not
configured, or its SDK raises for any reason, the wrapper degrades to a no-op and the
pipeline proceeds. Nothing in the answer path depends on a trace being written.
What is traced, per the spec, is *every model call*: the Haiku query gate and the Sonnet
generation, each with its inputs, outputs, model name and token usage — which is what
makes "why did it answer that?" answerable from the retrieval set rather than guessed.
"""
from __future__ import annotations
import logging
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Any, Protocol
from app.core.settings import Settings, get_settings
logger = logging.getLogger(__name__)
_CLIENT: Any | None = None
_INITIALISED = False
class Span(Protocol):
"""The subset of the Langfuse span surface this codebase uses."""
def update(self, **kwargs: Any) -> Any: ...
class _NullSpan:
"""Stand-in used whenever tracing is unavailable."""
def update(self, **kwargs: Any) -> None:
"""Accept and discard everything: this stands in for a span that is not there."""
del kwargs
NULL_SPAN = _NullSpan()
def get_langfuse(settings: Settings | None = None) -> Any | None:
"""Return a configured Langfuse client, or ``None`` when tracing is off."""
global _CLIENT, _INITIALISED # noqa: PLW0603 - process-wide singleton
if _INITIALISED:
return _CLIENT
cfg = settings or get_settings()
_INITIALISED = True
if not cfg.langfuse_enabled:
logger.info("Langfuse not configured; tracing disabled")
_CLIENT = None
return None
try:
from langfuse import Langfuse
_CLIENT = Langfuse(
public_key=cfg.langfuse_public_key,
secret_key=cfg.langfuse_secret_key,
host=cfg.langfuse_host,
)
logger.info("Langfuse tracing enabled (%s)", cfg.langfuse_host)
except Exception:
logger.warning("Langfuse initialisation failed; continuing untraced", exc_info=True)
_CLIENT = None
return _CLIENT
@contextmanager
def trace_span(name: str, **attributes: Any) -> Iterator[Span]:
"""Trace a pipeline stage. Yields a span that is safe to call unconditionally."""
client = get_langfuse()
if client is None:
yield NULL_SPAN
return
try:
with client.start_as_current_span(name=name, **attributes) as span:
yield span
except Exception:
logger.debug("trace_span(%s) failed; continuing untraced", name, exc_info=True)
yield NULL_SPAN
@contextmanager
def trace_generation(name: str, **attributes: Any) -> Iterator[Span]:
"""Trace a model call, recording model name, input, output and usage."""
client = get_langfuse()
if client is None:
yield NULL_SPAN
return
try:
with client.start_as_current_generation(name=name, **attributes) as span:
yield span
except Exception:
logger.debug("trace_generation(%s) failed; continuing untraced", name, exc_info=True)
yield NULL_SPAN
def flush() -> None:
"""Best-effort flush of pending traces, called on shutdown."""
client = get_langfuse()
if client is None:
return
try:
client.flush()
except Exception:
logger.debug("Langfuse flush failed", exc_info=True)
def reset() -> None:
"""Forget the cached client. Used by tests."""
global _CLIENT, _INITIALISED # noqa: PLW0603 - mirrors get_langfuse
_CLIENT = None
_INITIALISED = False