mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Merge fbd7a9285e into ed4caebb65
This commit is contained in:
commit
2fc3b841ed
2 changed files with 71 additions and 0 deletions
|
|
@ -133,6 +133,16 @@ class ServiceLogging(CustomLogger):
|
|||
except Exception as e:
|
||||
verbose_logger.exception("Error emitting service event - %s", e)
|
||||
|
||||
def _has_service_event_observers(self) -> bool:
|
||||
"""Whether anything would receive a service event sent from a blocking caller.
|
||||
|
||||
Sending one runs an event loop on the calling thread. Under gevent, every
|
||||
greenlet on that thread then sees a running loop, and Django rejects their
|
||||
database queries with ``SynchronousOnlyOperation``. So skip it when there is
|
||||
no service callback (or test counter) to receive the event.
|
||||
"""
|
||||
return bool(litellm.service_callback) or self.mock_testing
|
||||
|
||||
@staticmethod
|
||||
def _dispatch_from_sync(hook: Callable[[], Coroutine[object, object, None]]) -> None:
|
||||
"""Run an async service hook from a blocking caller, whatever event loop it holds.
|
||||
|
|
@ -167,6 +177,8 @@ class ServiceLogging(CustomLogger):
|
|||
|
||||
if self.mock_testing:
|
||||
self.mock_testing_sync_success_hook += 1
|
||||
if not self._has_service_event_observers():
|
||||
return
|
||||
|
||||
self._dispatch_from_sync(
|
||||
lambda: self.async_service_success_hook(
|
||||
|
|
@ -196,6 +208,8 @@ class ServiceLogging(CustomLogger):
|
|||
"""
|
||||
if self.mock_testing:
|
||||
self.mock_testing_sync_failure_hook += 1
|
||||
if not self._has_service_event_observers():
|
||||
return
|
||||
|
||||
self._dispatch_from_sync(
|
||||
lambda: self.async_service_failure_hook(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ Regression test for KeyError: 'call_type' when async_log_success_event
|
|||
is called without call_type in kwargs (e.g. from batch polling callbacks).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import asyncio.events
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
|
@ -277,3 +280,57 @@ async def test_service_failure_span_not_duplicated_for_string_and_instance(
|
|||
s for s in exporter.get_finished_spans() if s.name == "postgres get_user_object"
|
||||
]
|
||||
assert len(db_spans) == 1
|
||||
|
||||
|
||||
_SYNC_HOOK_CALLS = [
|
||||
pytest.param("service_success_hook", {}, id="success"),
|
||||
pytest.param("service_failure_hook", {"error": RuntimeError("boom")}, id="failure"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def started_loops(monkeypatch) -> list[asyncio.AbstractEventLoop]:
|
||||
"""Event loops started during the test."""
|
||||
started: list[asyncio.AbstractEventLoop] = []
|
||||
original = asyncio.events._set_running_loop
|
||||
|
||||
def recording_set_running_loop(loop):
|
||||
if loop is not None:
|
||||
started.append(loop)
|
||||
original(loop)
|
||||
|
||||
monkeypatch.setattr(asyncio.events, "_set_running_loop", recording_set_running_loop)
|
||||
return started
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hook_name, extra_kwargs", _SYNC_HOOK_CALLS)
|
||||
def test_sync_service_hook_starts_no_event_loop_without_callbacks(monkeypatch, started_loops, hook_name, extra_kwargs):
|
||||
"""With no service callback, nothing receives the event, so no loop should start."""
|
||||
monkeypatch.setattr(litellm, "service_callback", [])
|
||||
|
||||
getattr(ServiceLogging(), hook_name)(service=ServiceTypes.REDIS, duration=0.1, call_type="test", **extra_kwargs)
|
||||
|
||||
assert started_loops == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hook_name, extra_kwargs", _SYNC_HOOK_CALLS)
|
||||
def test_sync_service_hook_still_delivers_to_configured_callbacks(
|
||||
monkeypatch, hook_name, extra_kwargs
|
||||
):
|
||||
"""With a service callback, the event still reaches it: a real OTel logger exports the span."""
|
||||
from litellm.integrations.otel.model.spans import SpanRole
|
||||
|
||||
v2_logger, exporter = _make_otel_v2_logger()
|
||||
parent = v2_logger._emitter.start_span(SpanRole.PROXY_REQUEST, "POST /chat/completions")
|
||||
monkeypatch.setattr(litellm, "service_callback", [v2_logger])
|
||||
|
||||
getattr(ServiceLogging(), hook_name)(
|
||||
service=ServiceTypes.REDIS,
|
||||
duration=0.1,
|
||||
call_type="test",
|
||||
parent_otel_span=parent,
|
||||
**extra_kwargs,
|
||||
)
|
||||
parent.end()
|
||||
|
||||
assert "redis test" in [span.name for span in exporter.get_finished_spans()]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue