/ src / core / logging_config.py
logging_config.py
  1  """
  2  Logging configuration for Ag3ntum.
  3  
  4  Provides unified logging setup utilities for CLI, HTTP client, and API.
  5  All modules should use these functions instead of configuring logging directly.
  6  
  7  Usage:
  8      from .logging_config import setup_file_logging, setup_dual_logging
  9  
 10      # For CLI (file-only logging)
 11      setup_file_logging(log_level="INFO")
 12  
 13      # For API (console + file logging)
 14      setup_dual_logging(log_level="DEBUG", loggers=["src.api", "uvicorn"])
 15  """
 16  import logging
 17  import sys
 18  from logging.handlers import RotatingFileHandler
 19  from pathlib import Path
 20  from typing import Optional
 21  
 22  import colorlog
 23  
 24  from ..config import LOGS_DIR
 25  from .constants import (
 26      COLORLOG_COLORS,
 27      LOG_BACKUP_COUNT,
 28      LOG_FILE_BACKEND,
 29      LOG_FILE_CLI,
 30      LOG_FILE_HTTP,
 31      LOG_FORMAT_COLORED,
 32      LOG_FORMAT_FILE,
 33      LOG_MAX_BYTES,
 34  )
 35  
 36  logger = logging.getLogger(__name__)
 37  
 38  
 39  def _get_log_level(log_level: str) -> int:
 40      """
 41      Convert log level string to logging constant.
 42  
 43      Args:
 44          log_level: Log level name (DEBUG, INFO, WARNING, ERROR).
 45  
 46      Returns:
 47          Logging level constant.
 48      """
 49      return getattr(logging, log_level.upper(), logging.INFO)
 50  
 51  
 52  def _create_rotating_file_handler(
 53      log_file: Path,
 54      level: int,
 55      max_bytes: int = LOG_MAX_BYTES,
 56      backup_count: int = LOG_BACKUP_COUNT,
 57  ) -> RotatingFileHandler:
 58      """
 59      Create a rotating file handler with standard configuration.
 60  
 61      Args:
 62          log_file: Path to the log file.
 63          level: Logging level.
 64          max_bytes: Maximum file size before rotation.
 65          backup_count: Number of backup files to keep.
 66  
 67      Returns:
 68          Configured RotatingFileHandler.
 69      """
 70      log_file.parent.mkdir(parents=True, exist_ok=True)
 71  
 72      handler = RotatingFileHandler(
 73          filename=str(log_file),
 74          maxBytes=max_bytes,
 75          backupCount=backup_count,
 76          encoding="utf-8",
 77      )
 78      handler.setLevel(level)
 79      handler.setFormatter(logging.Formatter(LOG_FORMAT_FILE))
 80      return handler
 81  
 82  
 83  def _create_console_handler(level: int, colored: bool = False) -> logging.Handler:
 84      """
 85      Create a console (stdout) handler.
 86  
 87      Args:
 88          level: Logging level.
 89          colored: Whether to use colored output.
 90  
 91      Returns:
 92          Configured StreamHandler.
 93      """
 94      if colored:
 95          handler = colorlog.StreamHandler(sys.stdout)
 96          handler.setFormatter(
 97              colorlog.ColoredFormatter(
 98                  LOG_FORMAT_COLORED,
 99                  log_colors=COLORLOG_COLORS,
100                  secondary_log_colors={},
101                  style="%",
102              )
103          )
104      else:
105          handler = logging.StreamHandler(sys.stdout)
106          handler.setFormatter(logging.Formatter(LOG_FORMAT_FILE))
107  
108      handler.setLevel(level)
109      return handler
110  
111  
112  def setup_file_logging(
113      log_level: str = "INFO",
114      log_file: Optional[Path] = None,
115      log_name: str = LOG_FILE_CLI,
116      max_bytes: int = LOG_MAX_BYTES,
117      backup_count: int = LOG_BACKUP_COUNT,
118  ) -> None:
119      """
120      Configure file-only logging (for CLI and HTTP client).
121  
122      Replaces all handlers on the root logger with a single rotating file handler.
123      This ensures clean log separation between different entry points.
124  
125      Args:
126          log_level: Logging level (DEBUG, INFO, WARNING, ERROR).
127          log_file: Path to log file. If None, uses LOGS_DIR / log_name.
128          log_name: Name of log file (default: agent_cli.log).
129          max_bytes: Maximum size of log file before rotation.
130          backup_count: Number of backup files to keep.
131      """
132      level = _get_log_level(log_level)
133  
134      if log_file is None:
135          log_file = LOGS_DIR / log_name
136  
137      file_handler = _create_rotating_file_handler(
138          log_file=log_file,
139          level=level,
140          max_bytes=max_bytes,
141          backup_count=backup_count,
142      )
143  
144      root_logger = logging.getLogger()
145      root_logger.handlers.clear()
146      root_logger.setLevel(level)
147      root_logger.addHandler(file_handler)
148  
149  
150  def setup_dual_logging(
151      log_level: str = "INFO",
152      log_file: Optional[Path] = None,
153      log_name: str = LOG_FILE_BACKEND,
154      loggers: Optional[list[str]] = None,
155      max_bytes: int = LOG_MAX_BYTES,
156      backup_count: int = LOG_BACKUP_COUNT,
157  ) -> None:
158      """
159      Configure dual logging: colored console + rotating file.
160  
161      For API/backend use where you want both console output for development
162      and file logging for production. Configures specific loggers to prevent
163      log bleeding from other components.
164  
165      Args:
166          log_level: Logging level (DEBUG, INFO, WARNING, ERROR).
167          log_file: Path to log file. If None, uses LOGS_DIR / log_name.
168          log_name: Name of log file (default: backend.log).
169          loggers: List of logger names to configure. If None, uses root logger.
170          max_bytes: Maximum size of log file before rotation.
171          backup_count: Number of backup files to keep.
172      """
173      level = _get_log_level(log_level)
174  
175      if log_file is None:
176          log_file = LOGS_DIR / log_name
177  
178      file_handler = _create_rotating_file_handler(
179          log_file=log_file,
180          level=level,
181          max_bytes=max_bytes,
182          backup_count=backup_count,
183      )
184      console_handler = _create_console_handler(level=level, colored=True)
185  
186      if loggers is None:
187          # Configure root logger
188          root_logger = logging.getLogger()
189          root_logger.handlers.clear()
190          root_logger.setLevel(level)
191          root_logger.addHandler(file_handler)
192          root_logger.addHandler(console_handler)
193      else:
194          # Configure specific loggers (prevents log bleeding)
195          for logger_name in loggers:
196              log = logging.getLogger(logger_name)
197              log.handlers.clear()
198              log.setLevel(level)
199              log.addHandler(file_handler)
200              log.addHandler(console_handler)
201              log.propagate = False
202  
203  
204  def setup_cli_logging(log_level: str = "INFO") -> None:
205      """
206      Configure logging for CLI entry point (agent_cli.py).
207  
208      Shorthand for setup_file_logging with CLI defaults.
209  
210      Args:
211          log_level: Logging level.
212      """
213      setup_file_logging(log_level=log_level, log_name=LOG_FILE_CLI)
214  
215  
216  def setup_http_logging(log_level: str = "INFO") -> None:
217      """
218      Configure logging for HTTP client entry point (agent_http.py).
219  
220      Shorthand for setup_file_logging with HTTP client defaults.
221  
222      Args:
223          log_level: Logging level.
224      """
225      setup_file_logging(log_level=log_level, log_name=LOG_FILE_HTTP)
226  
227  
228  def setup_backend_logging(log_level: str = "INFO") -> None:
229      """
230      Configure logging for API backend (src/api/main.py).
231  
232      Uses dual logging (console + file) for specific backend loggers.
233  
234      Args:
235          log_level: Logging level.
236      """
237      backend_loggers = [
238          "src.api",
239          "src.services",
240          "src.core",
241          "src.db",
242          "ag3ntum",
243          "tools.ag3ntum",
244          "uvicorn",
245          "uvicorn.error",
246          "uvicorn.access",
247          "fastapi",
248      ]
249  
250      setup_dual_logging(
251          log_level=log_level,
252          log_name=LOG_FILE_BACKEND,
253          loggers=backend_loggers,
254      )
255  
256      # Enable uvicorn access logs for HTTP request tracking
257      logging.getLogger("uvicorn.access").setLevel(logging.INFO)