Skip to content

Conversation

@neubig
Copy link

@neubig neubig commented Jan 2, 2026

Relevant issues

Fixes asyncio exceptions being logged as plain text tracebacks instead of JSON when JSON_LOGS=true.

Problem

When JSON_LOGS=true, asyncio "Task exception was never retrieved" errors are logged as multi-line plain text instead of JSON. This breaks log aggregation in Datadog and other log management systems.

Example from Datadog (service:litellm-database):

Task exception was never retrieved
future: <Task finished name='Task-1' coro=<PrismaClient._check_db_connection() done...> exception=AttributeError("'NoneType' object has no attribute 'is_connected'")>
Traceback (most recent call last):
  File "...", line X, in _check_db_connection
    ...
AttributeError: 'NoneType' object has no attribute 'is_connected'

How to Reproduce

The asyncio "Task exception was never retrieved" error occurs when fire-and-forget tasks fail. These are internal background tasks (like _check_db_connection) that are created with asyncio.create_task() but never awaited.

Unit Test Reproduction

# Install from fix branch
pip install git+https://github.com/OpenHands/litellm.git@fix/json-logging-asyncio-exception-handler

# Run the unit tests
cd /path/to/litellm
pytest tests/litellm/logging/test_json_logging.py -v

The tests verify:

  1. test_async_json_exception_handler_logs_json_with_stacktrace - Handler produces JSON with stacktrace
  2. test_setup_asyncio_json_exception_handler_sets_handler_on_running_loop - Handler is set on the running event loop

Manual Reproduction (Python)

import asyncio
import os
os.environ["JSON_LOGS"] = "true"

import litellm
from litellm._logging import _setup_asyncio_json_exception_handler

async def failing_task():
    await asyncio.sleep(0.1)
    raise ValueError("Test exception from async task")

async def main():
    # This is what proxy_startup_event does
    _setup_asyncio_json_exception_handler()
    
    # Create a fire-and-forget task that will fail
    task = asyncio.create_task(failing_task())
    await asyncio.sleep(0.5)
    del task
    import gc
    gc.collect()
    await asyncio.sleep(0.2)

asyncio.run(main())

Without fix: Multi-line plain text traceback
With fix: Single-line JSON with stacktrace field

Root Cause

Two issues in litellm/_logging.py:

  1. Wrong event loop: The asyncio exception handler was set at module import time using asyncio.get_event_loop(). When uvicorn starts, it creates its own event loop, so the handler was set on the wrong loop. Asyncio exceptions fall back to the default handler which outputs plain text.

  2. Missing traceback: The original handler had exc_info=None, so even if it was on the correct loop, the JSON output would be missing the stacktrace field.

Fix

  • Added _setup_asyncio_json_exception_handler() that sets the handler on the running event loop
  • Call it in proxy_startup_event after uvicorn's event loop is created
  • Extract traceback from exception.__traceback__ and include it in JSON output via exc_info

Pre-Submission checklist

  • I have Added testing in the tests/litellm/ directory
  • My PR passes all unit tests
  • My PR includes a clear description of the problem and solution

This fixes two issues with JSON logging for asyncio exceptions:

1. **Primary issue (plain text tracebacks):** The asyncio exception handler was
   set at import time using asyncio.get_event_loop(). When uvicorn starts, it
   creates its own event loop, so the handler was set on the wrong loop. This
   caused asyncio exceptions to fall back to the default handler which logs
   plain text tracebacks across multiple lines.

2. **Secondary issue (missing stacktrace):** The original handler had
   exc_info=None, so even if it was on the correct loop, the JSON output would
   be missing the stacktrace field.

The fix:
- Added _setup_asyncio_json_exception_handler() that sets the handler on the
  running event loop
- Call it in proxy_startup_event after uvicorn's event loop is created
- Extract traceback from exception.__traceback__ and include it in JSON output
  via exc_info

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig
Copy link
Author

neubig commented Jan 2, 2026

Gonna close this because it's not obviously necessary after the litellm upgrade.

@neubig neubig closed this Jan 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants