-
-
Notifications
You must be signed in to change notification settings - Fork 11.6k
fix: drain datadog batches safely #25663
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
krrish-berri-2
merged 6 commits into
BerriAI:litellm_oss_staging_04_13_2026_p1
from
emerzon:fix/datadog-log-queue-leak
Apr 14, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
428787b
fix: drain datadog batches safely
emerzon af3eb66
fix: preserve datadog batches on 413
emerzon f13b3f0
fix: import time in datadog flush queue
emerzon 567125f
test: cover datadog batching edge cases
emerzon e7e7a39
fix: only stamp successful datadog flushes
emerzon c457a68
test: use sync mock for datadog payload builder
emerzon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
267 changes: 267 additions & 0 deletions
267
tests/test_litellm/integrations/datadog/test_datadog_logger_batching.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,267 @@ | ||
| from unittest.mock import AsyncMock, Mock, patch | ||
|
|
||
| import pytest | ||
| from httpx import Request, Response | ||
|
|
||
| from litellm.integrations.datadog.datadog import DataDogLogger | ||
| from litellm.types.integrations.datadog import DatadogPayload | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def datadog_env(monkeypatch): | ||
| monkeypatch.setenv("DD_API_KEY", "test_api_key") | ||
| monkeypatch.setenv("DD_SITE", "test.datadoghq.com") | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_async_send_batch_keeps_events_appended_during_send(datadog_env): | ||
| with patch("asyncio.create_task"): | ||
| logger = DataDogLogger() | ||
|
|
||
| logger.log_queue = [ | ||
| DatadogPayload( | ||
| ddsource="litellm", | ||
| ddtags="env:test", | ||
| hostname="host", | ||
| message=f'{{"event": {i}}}', | ||
| service="svc", | ||
| status="info", | ||
| ) | ||
| for i in range(2) | ||
| ] | ||
|
|
||
| async def _mock_send(data): | ||
| logger.log_queue.append( | ||
| DatadogPayload( | ||
| ddsource="litellm", | ||
| ddtags="env:test", | ||
| hostname="host", | ||
| message='{"event": 2}', | ||
| service="svc", | ||
| status="info", | ||
| ) | ||
| ) | ||
| return Response( | ||
| 202, request=Request("POST", "https://example.com"), text="Accepted" | ||
| ) | ||
|
|
||
| logger.async_send_compressed_data = AsyncMock(side_effect=_mock_send) | ||
|
|
||
| await logger.async_send_batch() | ||
|
|
||
| assert logger.async_send_compressed_data.await_count == 1 | ||
| sent_batch = logger.async_send_compressed_data.await_args.args[0] | ||
| assert len(sent_batch) == 2 | ||
| assert len(logger.log_queue) == 1 | ||
| assert logger.log_queue[0]["message"] == '{"event": 2}' | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_failure_hook_threshold_flush_uses_flush_queue(datadog_env): | ||
| with patch("asyncio.create_task"): | ||
| logger = DataDogLogger() | ||
|
|
||
| logger.batch_size = 1 | ||
| logger.flush_queue = AsyncMock() | ||
|
|
||
| await logger.async_post_call_failure_hook( | ||
| request_data={}, | ||
| original_exception=Exception("boom"), | ||
| user_api_key_dict=type("UserKey", (), {})(), | ||
| traceback_str="trace", | ||
| ) | ||
|
|
||
| logger.flush_queue.assert_awaited_once() | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_async_send_batch_requeues_events_on_413(datadog_env): | ||
| with patch("asyncio.create_task"): | ||
| logger = DataDogLogger() | ||
|
|
||
| logger.log_queue = [ | ||
| DatadogPayload( | ||
| ddsource="litellm", | ||
| ddtags="env:test", | ||
| hostname="host", | ||
| message=f'{{"event": {i}}}', | ||
| service="svc", | ||
| status="info", | ||
| ) | ||
| for i in range(2) | ||
| ] | ||
|
|
||
| logger.async_send_compressed_data = AsyncMock( | ||
| return_value=Response( | ||
| 413, | ||
| request=Request("POST", "https://example.com"), | ||
| text="Payload Too Large", | ||
| ) | ||
| ) | ||
|
|
||
| await logger.async_send_batch() | ||
|
|
||
| assert logger.async_send_compressed_data.await_count == 1 | ||
| assert len(logger.log_queue) == 2 | ||
| assert [event["message"] for event in logger.log_queue] == [ | ||
| '{"event": 0}', | ||
| '{"event": 1}', | ||
| ] | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_async_send_batch_handles_empty_queue(datadog_env): | ||
| with patch("asyncio.create_task"): | ||
| logger = DataDogLogger() | ||
|
|
||
| logger.log_queue = [] | ||
| logger.async_send_compressed_data = AsyncMock() | ||
|
|
||
| await logger.async_send_batch() | ||
|
|
||
| logger.async_send_compressed_data.assert_not_awaited() | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_async_send_batch_requeues_events_on_exception(datadog_env): | ||
| with patch("asyncio.create_task"): | ||
| logger = DataDogLogger() | ||
|
|
||
| logger.log_queue = [ | ||
| DatadogPayload( | ||
| ddsource="litellm", | ||
| ddtags="env:test", | ||
| hostname="host", | ||
| message=f'{{"event": {i}}}', | ||
| service="svc", | ||
| status="info", | ||
| ) | ||
| for i in range(2) | ||
| ] | ||
|
|
||
| logger.async_send_compressed_data = AsyncMock(side_effect=RuntimeError("boom")) | ||
|
|
||
| await logger.async_send_batch() | ||
|
|
||
| assert [event["message"] for event in logger.log_queue] == [ | ||
| '{"event": 0}', | ||
| '{"event": 1}', | ||
| ] | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_log_async_event_threshold_flush_uses_flush_queue(datadog_env): | ||
| with patch("asyncio.create_task"): | ||
| logger = DataDogLogger() | ||
|
|
||
| logger.batch_size = 1 | ||
| logger.flush_queue = AsyncMock() | ||
| logger.create_datadog_logging_payload = Mock( | ||
| return_value=DatadogPayload( | ||
| ddsource="litellm", | ||
| ddtags="env:test", | ||
| hostname="host", | ||
| message='{"event": 0}', | ||
| service="svc", | ||
| status="info", | ||
| ) | ||
| ) | ||
|
|
||
| await logger._log_async_event( | ||
| kwargs={}, | ||
| response_obj={}, | ||
| start_time=None, | ||
| end_time=None, | ||
| ) | ||
|
|
||
| logger.flush_queue.assert_awaited_once() | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_flush_queue_updates_last_flush_time(datadog_env): | ||
| with patch("asyncio.create_task"): | ||
| logger = DataDogLogger() | ||
|
|
||
| logger.log_queue = [ | ||
| DatadogPayload( | ||
| ddsource="litellm", | ||
| ddtags="env:test", | ||
| hostname="host", | ||
| message='{"event": 0}', | ||
| service="svc", | ||
| status="info", | ||
| ) | ||
| ] | ||
| logger.last_flush_time = 0 | ||
|
|
||
| async def _successful_send(): | ||
| logger.log_queue = [] | ||
|
|
||
| logger.async_send_batch = AsyncMock(side_effect=_successful_send) | ||
|
|
||
| await logger.flush_queue() | ||
|
|
||
| logger.async_send_batch.assert_awaited_once() | ||
| assert logger.last_flush_time > 0 | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_flush_queue_does_not_update_last_flush_time_when_send_requeues( | ||
| datadog_env, | ||
| ): | ||
| with patch("asyncio.create_task"): | ||
| logger = DataDogLogger() | ||
|
|
||
| logger.log_queue = [ | ||
| DatadogPayload( | ||
| ddsource="litellm", | ||
| ddtags="env:test", | ||
| hostname="host", | ||
| message='{"event": 0}', | ||
| service="svc", | ||
| status="info", | ||
| ) | ||
| ] | ||
| logger.last_flush_time = 123.0 | ||
|
|
||
| async def _requeue_batch(): | ||
| logger.log_queue = [ | ||
| DatadogPayload( | ||
| ddsource="litellm", | ||
| ddtags="env:test", | ||
| hostname="host", | ||
| message='{"event": 0}', | ||
| service="svc", | ||
| status="info", | ||
| ) | ||
| ] | ||
|
|
||
| logger.async_send_batch = AsyncMock(side_effect=_requeue_batch) | ||
|
|
||
| await logger.flush_queue() | ||
|
|
||
| logger.async_send_batch.assert_awaited_once() | ||
| assert logger.last_flush_time == 123.0 | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_flush_queue_returns_without_lock(datadog_env): | ||
| with patch("asyncio.create_task"): | ||
| logger = DataDogLogger() | ||
|
|
||
| logger.flush_lock = None | ||
| logger.log_queue = [ | ||
| DatadogPayload( | ||
| ddsource="litellm", | ||
| ddtags="env:test", | ||
| hostname="host", | ||
| message='{"event": 0}', | ||
| service="svc", | ||
| status="info", | ||
| ) | ||
| ] | ||
| logger.async_send_batch = AsyncMock() | ||
|
|
||
| await logger.flush_queue() | ||
|
|
||
| logger.async_send_batch.assert_not_awaited() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.