/ server / opensandbox_server / logging_config.py
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