[Bug fix] JSON logs - Ensure only 1 log is emitted (previously duplicate json logs were getting emitted) (#10580)

* fix logging with JSON logs

* test: test_initialize_loggers_with_handler_sets_propagate_false
This commit is contained in:
Ishaan Jaff
2025-05-05 20:25:47 -07:00
committed by GitHub
parent ddc24da2a7
commit cd48ffb6c4
3 changed files with 98 additions and 22 deletions
+25 -20
View File
@@ -103,31 +103,36 @@ verbose_proxy_logger = logging.getLogger("LiteLLM Proxy")
verbose_router_logger = logging.getLogger("LiteLLM Router")
verbose_logger = logging.getLogger("LiteLLM")
# Add the handler to the logger
verbose_router_logger.addHandler(handler)
verbose_proxy_logger.addHandler(handler)
verbose_logger.addHandler(handler)
ALL_LOGGERS = [
logging.getLogger(),
verbose_logger,
verbose_router_logger,
verbose_proxy_logger,
]
def _initialize_loggers_with_handler(handler: logging.Handler):
"""
Initialize all loggers with a handler
- Adds a handler to each logger
- Prevents bubbling to parent/root (critical to prevent duplicate JSON logs)
"""
for lg in ALL_LOGGERS:
lg.handlers.clear() # remove any existing handlers
lg.addHandler(handler) # add JSON formatter handler
lg.propagate = False # prevent bubbling to parent/root
def _turn_on_json():
"""
Turn on JSON logging
- Adds a JSON formatter to all loggers
"""
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
# Define all loggers to update, including root logger
loggers = [logging.getLogger()] + [
verbose_router_logger,
verbose_proxy_logger,
verbose_logger,
]
# Iterate through each logger and update its handlers
for logger in loggers:
# Remove all existing handlers
for h in logger.handlers[:]:
logger.removeHandler(h)
# Add the new handler
logger.addHandler(handler)
_initialize_loggers_with_handler(handler)
# Set up exception handlers
_setup_json_exception_handlers(JsonFormatter())
+2 -2
View File
@@ -4,6 +4,6 @@ model_list:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
litellm_settings:
success_callback: ["generic_api"]
litellm_settings:
json_logs: true
+71
View File
@@ -0,0 +1,71 @@
import datetime
import json
import os
import sys
import unittest
from typing import List, Optional, Tuple
from unittest.mock import ANY, MagicMock, Mock, patch
import httpx
import pytest
sys.path.insert(
0, os.path.abspath("../../..")
) # Adds the parent directory to the system-path
import io
import logging
import sys
import unittest
from contextlib import redirect_stdout
import litellm
from litellm._logging import (
ALL_LOGGERS,
_initialize_loggers_with_handler,
verbose_logger,
verbose_proxy_logger,
verbose_router_logger,
)
def test_json_mode_emits_one_record_per_logger(capfd):
# Turn on JSON logging
litellm._logging._turn_on_json()
# Make sure our loggers will emit INFO-level records
for lg in (verbose_logger, verbose_router_logger, verbose_proxy_logger):
lg.setLevel(logging.INFO)
# Log one message from each logger at different levels
verbose_logger.info("first info")
verbose_router_logger.info("second info from router")
verbose_proxy_logger.info("third info from proxy")
# Capture stdout
out, err = capfd.readouterr()
print("out", out)
print("err", err)
lines = [l for l in err.splitlines() if l.strip()]
# Expect exactly three JSON lines
assert len(lines) == 3, f"got {len(lines)} lines, want 3: {lines!r}"
# Each line must be valid JSON with the required fields
for line in lines:
obj = json.loads(line)
assert "message" in obj, "`message` key missing"
assert "level" in obj, "`level` key missing"
assert "timestamp" in obj, "`timestamp` key missing"
def test_initialize_loggers_with_handler_sets_propagate_false():
"""
Test that the initialize_loggers_with_handler function sets propagate to False for all loggers
"""
# Initialize loggers with the test handler
_initialize_loggers_with_handler(logging.StreamHandler())
# Check that propagate is set to False for all loggers
for logger in ALL_LOGGERS:
assert (
logger.propagate is False
), f"Logger {logger.name} has propagate set to {logger.propagate}, expected False"