diff --git a/litellm/_logging.py b/litellm/_logging.py index d7e2c9e778..3a7f5bc31e 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -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()) diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index 58edfefcb6..1cfa97a656 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -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 \ No newline at end of file diff --git a/tests/litellm/test_logging.py b/tests/litellm/test_logging.py new file mode 100644 index 0000000000..6044bd2e69 --- /dev/null +++ b/tests/litellm/test_logging.py @@ -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"