From 73ba764922ff6259799f812a2639afd6bbb31d0b Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 10 Jul 2026 11:16:27 +0300 Subject: [PATCH] Emit a docling_serve.request span per instance attempt --- CHANGELOG.md | 1 + .../haiku/rag/providers/docling_serve.py | 11 +++++-- tests/test_docling_serve_client.py | 30 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 548b70c4..309d9eff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changed - Logfire spans carry `service.version` and a per-process `service.name` (`haiku-ingester`, `haiku-rag`, `haiku-rag-app`); `OTEL_SERVICE_NAME` / `LOGFIRE_SERVICE_NAME` override the default. +- Each docling-serve request emits a `docling_serve.request` span carrying the instance `url` and `attempt`. ## [0.65.0] - 2026-07-09 diff --git a/haiku_rag_slim/haiku/rag/providers/docling_serve.py b/haiku_rag_slim/haiku/rag/providers/docling_serve.py index 7a9164ec..a14cc0fe 100644 --- a/haiku_rag_slim/haiku/rag/providers/docling_serve.py +++ b/haiku_rag_slim/haiku/rag/providers/docling_serve.py @@ -11,6 +11,7 @@ import httpx from haiku.rag.circuit_breaker import CircuitBreaker from haiku.rag.config import CircuitBreakerConfig, DoclingServeConfig +from haiku.rag.telemetry import logfire logger = logging.getLogger(__name__) @@ -156,8 +157,14 @@ class DoclingServeClient: base_url = self._pick_url(exclude=frozenset(tried)) breaker = self._breaker_for(base_url) try: - async with self._httpx_client() as client: - result = await attempt(client, base_url) + with logfire.span( + "docling_serve.request", + name=name, + url=base_url, + attempt=attempt_no, + ): + async with self._httpx_client() as client: + result = await attempt(client, base_url) except Exception as exc: if not _is_retryable(exc): raise diff --git a/tests/test_docling_serve_client.py b/tests/test_docling_serve_client.py index 8bda6d61..ceaf89ee 100644 --- a/tests/test_docling_serve_client.py +++ b/tests/test_docling_serve_client.py @@ -301,6 +301,36 @@ async def test_task_failure_is_not_retried(): assert set(seen) == {"h"} +@pytest.mark.asyncio +async def test_request_span_records_instance_per_attempt(monkeypatch): + """Each attempt opens a docling_serve.request span tagged with the + instance URL, so failover is traceable in Logfire.""" + from contextlib import nullcontext + + from haiku.rag.providers import docling_serve as ds_module + + spans: list[dict] = [] + + def _fake_span(span_name, /, **attrs): + spans.append({"span_name": span_name, **attrs}) + return nullcontext() + + monkeypatch.setattr(ds_module.logfire, "span", _fake_span) + + transport, _ = _failover_transport({"down-s"}, "t", {"ok": True}) + client = DoclingServeClient( + base_urls=["http://down-s:5001", "http://up-s:5001"], + transport=transport, + retry_base_delay=0.0, + ) + + await _poll(client) + + requests = [s for s in spans if s["span_name"] == "docling_serve.request"] + assert [s["url"] for s in requests] == ["http://down-s:5001", "http://up-s:5001"] + assert [s["attempt"] for s in requests] == [0, 1] + + def test_pick_url_skips_excluded_instances(): """On retry, _pick_url advances past every excluded instance.""" urls = ["http://p1:5001", "http://p2:5001", "http://p3:5001"]