2025-09-04 15:44:54 -04:00

122 lines
3.4 KiB
Python

import json
import logging
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import TextIO
from pythonjsonlogger.json import JsonFormatter
from openhands.core.logger import openhands_logger
LOG_JSON = os.getenv('LOG_JSON', '1') == '1'
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
DEBUG = os.getenv('DEBUG', 'False').lower() in ['true', '1', 'yes']
if DEBUG:
LOG_LEVEL = 'DEBUG'
FILE_PREFIX = 'File "'
CWD_PREFIX = FILE_PREFIX + str(Path(os.getcwd()).parent) + '/'
SITE_PACKAGES_PREFIX = (
CWD_PREFIX
+ f'.venv/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages/'
)
# Make the JSON easy to read in the console - useful for non cloud environments
LOG_JSON_FOR_CONSOLE = int(os.getenv('LOG_JSON_FOR_CONSOLE', '0'))
def format_stack(stack: str) -> list[str]:
return (
stack.replace(SITE_PACKAGES_PREFIX, FILE_PREFIX)
.replace(CWD_PREFIX, FILE_PREFIX)
.replace('"', "'")
.split('\n')
)
def custom_json_serializer(obj, **kwargs):
if LOG_JSON_FOR_CONSOLE:
# Format json output
kwargs['indent'] = 2
obj = {'ts': datetime.now().isoformat(), **obj}
# Format stack traces
if isinstance(obj, dict):
exc_info = obj.get('exc_info')
if isinstance(exc_info, str):
obj['exc_info'] = format_stack(exc_info)
stack_info = obj.get('stack_info')
if isinstance(stack_info, str):
obj['stack_info'] = format_stack(stack_info)
result = json.dumps(obj, **kwargs)
return result
def setup_json_logger(
logger: logging.Logger,
level: str = LOG_LEVEL,
_out: TextIO = sys.stdout,
) -> None:
"""
Configure logger instance to output json for Google Cloud.
Existing filters should stay in place for sensitive content.
"""
# Remove existing handlers to avoid duplicate logs
for handler in logger.handlers[:]:
logger.removeHandler(handler)
handler = logging.StreamHandler(_out)
handler.setLevel(level)
formatter = JsonFormatter(
'{message}{levelname}',
style='{',
rename_fields={'levelname': 'severity'},
json_serializer=custom_json_serializer,
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(level)
def setup_all_loggers():
"""
Setup JSON logging for all libraries that may be logging.
Leave OpenHands alone since it's already configured.
"""
if LOG_JSON:
# Setup the root logger
setup_json_logger(logging.getLogger())
for name in logging.root.manager.loggerDict:
logger = logging.getLogger(name)
setup_json_logger(logger)
logger.propagate = False
# Quiet down some of the loggers that talk too much!
loquacious_loggers = {
'engineio',
'engineio.server',
'fastmcp',
'FastMCP',
'httpx',
'mcp.client.sse',
'socketio',
'socketio.client',
'socketio.server',
'sqlalchemy.engine.Engine',
'sqlalchemy.orm.mapper.Mapper',
}
for logger_name in loquacious_loggers:
logging.getLogger(logger_name).setLevel('WARNING')
logger = logging.getLogger('saas')
setup_all_loggers()
# Openhands logger is heavily customized - so we want to make sure that it is logging json
setup_json_logger(openhands_logger)