mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-11 03:04:02 +00:00
[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:
+25
-20
@@ -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())
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user