This commit is contained in:
Nao Yokotsuka 2025-04-24 00:55:00 -07:00 committed by GitHub
commit 97456d2e8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 169 additions and 3 deletions

View file

@ -0,0 +1,69 @@
import logging
import traceback
from datetime import datetime, UTC
import json
from uvicorn.config import LOGGING_CONFIG
# Override uvicorn's default logging config to use our JSON formatter
def uvicorn_json_log_config():
config = LOGGING_CONFIG.copy()
config['formatters'] = {
'default': {
'()': 'litellm.proxy.json_logging.JsonFormatter'
},
'access': {
'()': 'litellm.proxy.json_logging.AccessJsonFormatter'
}
}
return config
class JsonFormatter(logging.Formatter):
def format(self, record):
log_entry = {
"timestamp": datetime.fromtimestamp(record.created, UTC).isoformat(timespec='milliseconds') + 'Z',
"level": record.levelname,
"message": record.getMessage(),
"logger_name": record.name,
"process": record.process,
"thread": record.threadName,
}
# Add exception information
if record.exc_info:
log_entry['exception'] = "".join(traceback.format_exception(*record.exc_info))
exc_type, exc_value, exc_traceback = record.exc_info
log_entry['exception'] = {
'type': str(exc_type.__name__),
'message': str(exc_value),
'traceback': traceback.format_tb(exc_traceback)
}
# Add extra data
if hasattr(record, 'extra_data') and isinstance(record.extra_data, dict):
log_entry.update(record.extra_data) # extra データの処理例
return json.dumps(log_entry, ensure_ascii=False)
# ref: https://github.com/encode/uvicorn/blob/0.34.1/uvicorn/logging.py#L73
class AccessJsonFormatter(logging.Formatter):
def format(self, record):
(
client_addr,
method,
full_path,
http_version,
status_code,
) = record.args
log_entry = {
'timestamp': datetime.fromtimestamp(record.created, UTC).isoformat(timespec='milliseconds') + 'Z',
'level': record.levelname,
'logger_name': record.name,
'process': record.process,
'thread': record.threadName,
'client_addr': client_addr,
'method': method,
'full_path': full_path,
'http_version': http_version,
'status_code': status_code,
}
return json.dumps(log_entry, ensure_ascii=False)

View file

@ -132,8 +132,8 @@ class ProxyInitializationHelpers:
print(f"Using log_config: {log_config}") # noqa
uvicorn_args["log_config"] = log_config
elif litellm.json_logs:
print("Using json logs. Setting log_config to None.") # noqa
uvicorn_args["log_config"] = None
from litellm.proxy.json_logging import uvicorn_json_log_config
uvicorn_args["log_config"] = uvicorn_json_log_config()
return uvicorn_args
@staticmethod

View file

@ -108,7 +108,8 @@ class TestProxyInitializationHelpers:
args = ProxyInitializationHelpers._get_default_unvicorn_init_args(
"localhost", 8000
)
assert args["log_config"] is None
assert args["log_config"]["formatters"]["default"]["()"] == "litellm.proxy.json_logging.JsonFormatter"
assert args["log_config"]["formatters"]["access"]["()"] == "litellm.proxy.json_logging.AccessJsonFormatter"
@patch("asyncio.run")
@patch("builtins.print")

View file

@ -0,0 +1,96 @@
import unittest
import logging
import json
from datetime import datetime
from litellm.proxy.json_logging import JsonFormatter, AccessJsonFormatter
class TestJsonFormatter(unittest.TestCase):
def setUp(self):
self.formatter = JsonFormatter()
def test_basic_log_format(self):
record = logging.LogRecord(
name="test_logger",
level=logging.INFO,
pathname="test.py",
lineno=1,
msg="Test message",
args=(),
exc_info=None
)
formatted = self.formatter.format(record)
log_entry = json.loads(formatted)
self.assertEqual(log_entry["level"], "INFO")
self.assertEqual(log_entry["message"], "Test message")
self.assertEqual(log_entry["logger_name"], "test_logger")
self.assertIn("timestamp", log_entry)
self.assertIn("process", log_entry)
self.assertIn("thread", log_entry)
def test_log_with_exception(self):
try:
raise ValueError("Test error")
except ValueError as e:
record = logging.LogRecord(
name="test_logger",
level=logging.ERROR,
pathname="test.py",
lineno=1,
msg="Test error occurred",
args=(),
exc_info=(type(e), e, e.__traceback__)
)
formatted = self.formatter.format(record)
log_entry = json.loads(formatted)
self.assertEqual(log_entry["level"], "ERROR")
self.assertEqual(log_entry["message"], "Test error occurred")
self.assertIn("exception", log_entry)
self.assertEqual(log_entry["exception"]["type"], "ValueError")
self.assertEqual(log_entry["exception"]["message"], "Test error")
self.assertIsInstance(log_entry["exception"]["traceback"], list)
def test_log_with_extra_data(self):
record = logging.LogRecord(
name="test_logger",
level=logging.INFO,
pathname="test.py",
lineno=1,
msg="Test message",
args=(),
exc_info=None
)
record.extra_data = {"user_id": "123", "action": "login"}
formatted = self.formatter.format(record)
log_entry = json.loads(formatted)
self.assertEqual(log_entry["user_id"], "123")
self.assertEqual(log_entry["action"], "login")
class TestAccessJsonFormatter(unittest.TestCase):
def setUp(self):
self.formatter = AccessJsonFormatter()
def test_access_log_format(self):
record = logging.LogRecord(
name="uvicorn.access",
level=logging.INFO,
pathname="test.py",
lineno=1,
msg="%s - %s %s HTTP/%s %d",
args=("127.0.0.1", "GET", "/test", "1.1", 200),
exc_info=None
)
formatted = self.formatter.format(record)
log_entry = json.loads(formatted)
self.assertEqual(log_entry["level"], "INFO")
self.assertEqual(log_entry["client_addr"], "127.0.0.1")
self.assertEqual(log_entry["method"], "GET")
self.assertEqual(log_entry["full_path"], "/test")
self.assertEqual(log_entry["http_version"], "1.1")
self.assertEqual(log_entry["status_code"], 200)
self.assertIn("timestamp", log_entry)
self.assertIn("process", log_entry)
self.assertIn("thread", log_entry)