mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-24 18:24:20 +00:00
If json_logs: true, the proxy server will outputs access logs in JSON
This commit is contained in:
parent
14bcc9a6c9
commit
29a268f1d8
4 changed files with 169 additions and 3 deletions
69
litellm/proxy/json_logging.py
Normal file
69
litellm/proxy/json_logging.py
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
96
tests/proxy_unit_tests/test_json_logging.py
Normal file
96
tests/proxy_unit_tests/test_json_logging.py
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue