logging_config.py
1 # Copyright 2025 Alibaba Group Holding Ltd. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 """ 16 Logging configuration for the OpenSandbox server. 17 18 Two output modes: 19 20 stdout (default) 21 All loggers write to stdout via uvicorn's ColorizingFormatter, 22 which supports %(levelprefix)s (colored, padded level name). 23 24 file (when log_cfg.file_enabled is true) 25 All loggers write to a rotating file. %(levelprefix)s is replaced 26 with %(levelname)-8s because the standard logging.Formatter does not 27 inject that uvicorn-only field. 28 29 Optionally, uvicorn.access can be separated into its own rotating file 30 (log_cfg.access_file_path) following the Nginx/Gunicorn convention. 31 32 urllib3 InsecureRequestWarning is silenced at startup because it comes from 33 expected unverified HTTPS calls (e.g. in-cluster k8s API) and produces 34 high-frequency noise without actionable content. 35 """ 36 37 import copy 38 import logging 39 import logging.config 40 from pathlib import Path 41 42 import urllib3 43 from uvicorn.config import LOGGING_CONFIG as UVICORN_LOGGING_CONFIG 44 45 from opensandbox_server.config import LogConfig 46 47 # %(levelprefix)s: colored, padded label injected by uvicorn's ColorizingFormatter. 48 _STDOUT_FMT = "%(levelprefix)s %(asctime)s [%(request_id)s] %(name)s: %(message)s" 49 # %(levelname)-8s: plain, left-aligned label (width=8) for the standard Formatter. 50 _FILE_FMT = "%(levelname)-8s %(asctime)s [%(request_id)s] %(name)s: %(message)s" 51 _DATEFMT = "%Y-%m-%d %H:%M:%S%z" 52 53 # dictConfig factory string for RequestIdFilter. 54 _REQUEST_ID_FILTER = {"()": "opensandbox_server.middleware.request_id.RequestIdFilter"} 55 56 57 def _rotating_file_handler(filename: str, log_cfg: LogConfig) -> dict: 58 """Return a dictConfig handler entry for a RotatingFileHandler.""" 59 return { 60 "class": "logging.handlers.RotatingFileHandler", 61 "formatter": "file", 62 "filters": ["request_id"], 63 "filename": filename, 64 "maxBytes": log_cfg.file_max_bytes, 65 "backupCount": log_cfg.file_backup_count, 66 "encoding": "utf-8", 67 } 68 69 70 def _apply_stdout_config(log_config: dict, level: str) -> None: 71 """Patch uvicorn's stdout handlers/formatters with unified format and request_id filter.""" 72 for name in ("default", "access"): 73 log_config["formatters"][name]["fmt"] = _STDOUT_FMT 74 log_config["formatters"][name]["datefmt"] = _DATEFMT 75 log_config["formatters"][name]["use_colors"] = True 76 77 log_config["handlers"]["default"]["filters"] = ["request_id"] 78 log_config["handlers"]["access"]["filters"] = ["request_id"] 79 80 log_config["loggers"]["opensandbox_server"] = { 81 "handlers": ["default"], 82 "level": level, 83 "propagate": False, 84 } 85 86 87 def _apply_file_config(log_config: dict, log_cfg: LogConfig, level: str) -> None: 88 """ 89 Register file handlers and route all loggers to them. 90 91 uvicorn.access is routed to access_file when resolved_access_file_path() returns 92 a path, otherwise it shares the main file handler with the rest. 93 """ 94 # Use resolved paths (defaults applied when file_enabled=true and paths not set). 95 file_path = log_cfg.resolved_file_path() 96 access_file_path = log_cfg.resolved_access_file_path() 97 98 # Ensure parent directories exist. 99 Path(file_path).parent.mkdir(parents=True, exist_ok=True) 100 101 log_config["formatters"]["file"] = {"format": _FILE_FMT, "datefmt": _DATEFMT} 102 log_config["handlers"]["file"] = _rotating_file_handler(file_path, log_cfg) 103 104 if access_file_path: 105 Path(access_file_path).parent.mkdir(parents=True, exist_ok=True) 106 log_config["handlers"]["access_file"] = _rotating_file_handler( 107 access_file_path, log_cfg 108 ) 109 access_handler = "access_file" 110 else: 111 access_handler = "file" 112 113 for logger_name in ("uvicorn", "uvicorn.error", "opensandbox_server"): 114 log_config["loggers"].setdefault(logger_name, {}) 115 log_config["loggers"][logger_name]["handlers"] = ["file"] 116 log_config["loggers"][logger_name]["propagate"] = False 117 118 log_config["loggers"].setdefault("uvicorn.access", {}) 119 log_config["loggers"]["uvicorn.access"]["handlers"] = [access_handler] 120 log_config["loggers"]["uvicorn.access"]["propagate"] = False 121 122 log_config["loggers"]["opensandbox_server"]["level"] = level 123 124 125 def configure_logging(log_cfg: LogConfig) -> dict: 126 """ 127 Build and apply the server logging configuration. 128 129 Returns the final dictConfig dict for reuse by callers (e.g. uvicorn.run). 130 """ 131 # Silence high-frequency noise from expected unverified HTTPS calls. 132 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 133 134 level = log_cfg.level.upper() 135 log_config = copy.deepcopy(UVICORN_LOGGING_CONFIG) 136 137 # Register the request_id filter so any handler can reference it by name. 138 log_config["filters"] = {"request_id": _REQUEST_ID_FILTER} 139 140 if log_cfg.resolved_file_path(): 141 _apply_file_config(log_config, log_cfg, level) 142 else: 143 _apply_stdout_config(log_config, level) 144 145 logging.config.dictConfig(log_config) 146 logging.getLogger().setLevel(getattr(logging, level, logging.INFO)) 147 148 return log_config