Add Vision image fetch failure observability
This commit is contained in:
@@ -189,34 +189,64 @@ def _col(name: Optional[str]) -> str:
|
||||
return name or COLLECTION_NAME
|
||||
|
||||
|
||||
def _safe_clip_detail(response: httpx.Response) -> str:
|
||||
"""Extract only a bounded machine-readable category; never log response text."""
|
||||
try:
|
||||
detail = response.json().get("detail")
|
||||
except Exception:
|
||||
return "non_json_response"
|
||||
if not isinstance(detail, str):
|
||||
return "structured_error"
|
||||
import re
|
||||
match = re.search(r"(?:error_type|category)[=: ]+([a-z_]+)", detail)
|
||||
return match.group(1)[:40] if match else "upstream_error"
|
||||
|
||||
|
||||
def _log_clip_failure(operation: str, *, elapsed_ms: float, status: int | None = None, error_type: str, safe_detail: str | None = None) -> None:
|
||||
fields = {"event": "qdrant_clip_upstream_failed", "upstream": "clip", "operation": operation, "elapsed_ms": round(elapsed_ms, 1), "error_type": error_type}
|
||||
if status is not None:
|
||||
fields["upstream_status"] = status
|
||||
if safe_detail is not None:
|
||||
fields["safe_detail"] = safe_detail
|
||||
logger.warning("%s", " ".join(f"{key}={value!r}" for key, value in fields.items()))
|
||||
|
||||
|
||||
async def _embed_url(url: str) -> List[float]:
|
||||
"""Call the CLIP service to get an image embedding."""
|
||||
started_at = time.perf_counter()
|
||||
async with httpx.AsyncClient(timeout=30) as http:
|
||||
try:
|
||||
r = await http.post(f"{CLIP_URL}/embed", json={"url": url})
|
||||
except httpx.RequestError as e:
|
||||
_log_clip_failure("embed_url", elapsed_ms=(time.perf_counter() - started_at) * 1000, error_type=type(e).__name__)
|
||||
raise HTTPException(502, f"CLIP request failed: {str(e)}")
|
||||
if r.status_code >= 400:
|
||||
_log_clip_failure("embed_url", elapsed_ms=(time.perf_counter() - started_at) * 1000, status=r.status_code, error_type="upstream_http_error", safe_detail=_safe_clip_detail(r))
|
||||
raise HTTPException(502, f"CLIP /embed error: {r.status_code} {r.text[:200]}")
|
||||
try:
|
||||
return r.json()["vector"]
|
||||
except Exception:
|
||||
_log_clip_failure("embed_url", elapsed_ms=(time.perf_counter() - started_at) * 1000, status=r.status_code, error_type="invalid_response", safe_detail=_safe_clip_detail(r))
|
||||
raise HTTPException(502, f"CLIP /embed returned non-JSON: {r.status_code} {r.text[:200]}")
|
||||
|
||||
|
||||
async def _embed_bytes(data: bytes) -> List[float]:
|
||||
"""Call the CLIP service to embed uploaded file bytes."""
|
||||
started_at = time.perf_counter()
|
||||
async with httpx.AsyncClient(timeout=30) as http:
|
||||
files = {"file": ("image", data, "application/octet-stream")}
|
||||
try:
|
||||
r = await http.post(f"{CLIP_URL}/embed/file", files=files)
|
||||
except httpx.RequestError as e:
|
||||
_log_clip_failure("embed_file", elapsed_ms=(time.perf_counter() - started_at) * 1000, error_type=type(e).__name__)
|
||||
raise HTTPException(502, f"CLIP request failed: {str(e)}")
|
||||
if r.status_code >= 400:
|
||||
_log_clip_failure("embed_file", elapsed_ms=(time.perf_counter() - started_at) * 1000, status=r.status_code, error_type="upstream_http_error", safe_detail=_safe_clip_detail(r))
|
||||
raise HTTPException(502, f"CLIP /embed/file error: {r.status_code} {r.text[:200]}")
|
||||
try:
|
||||
return r.json()["vector"]
|
||||
except Exception:
|
||||
_log_clip_failure("embed_file", elapsed_ms=(time.perf_counter() - started_at) * 1000, status=r.status_code, error_type="invalid_response", safe_detail=_safe_clip_detail(r))
|
||||
raise HTTPException(502, f"CLIP /embed/file returned non-JSON: {r.status_code} {r.text[:200]}")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user