| | """Logging configuration for CiteScan.""" |
| | import logging |
| | import sys |
| | from pathlib import Path |
| | from typing import Any |
| | import json |
| | from datetime import datetime |
| |
|
| | from .config import settings |
| |
|
| |
|
| | class JSONFormatter(logging.Formatter): |
| | """Custom JSON formatter for structured logging.""" |
| |
|
| | def format(self, record: logging.LogRecord) -> str: |
| | """Format log record as JSON.""" |
| | log_data: dict[str, Any] = { |
| | "timestamp": datetime.utcnow().isoformat(), |
| | "level": record.levelname, |
| | "logger": record.name, |
| | "message": record.getMessage(), |
| | "module": record.module, |
| | "function": record.funcName, |
| | "line": record.lineno, |
| | } |
| |
|
| | |
| | if record.exc_info: |
| | log_data["exception"] = self.formatException(record.exc_info) |
| |
|
| | |
| | if hasattr(record, "extra"): |
| | log_data.update(record.extra) |
| |
|
| | return json.dumps(log_data, ensure_ascii=False) |
| |
|
| |
|
| | class TextFormatter(logging.Formatter): |
| | """Custom text formatter with colors for console output.""" |
| |
|
| | COLORS = { |
| | "DEBUG": "\033[36m", |
| | "INFO": "\033[32m", |
| | "WARNING": "\033[33m", |
| | "ERROR": "\033[31m", |
| | "CRITICAL": "\033[35m", |
| | } |
| | RESET = "\033[0m" |
| |
|
| | def format(self, record: logging.LogRecord) -> str: |
| | """Format log record with colors.""" |
| | color = self.COLORS.get(record.levelname, self.RESET) |
| | record.levelname = f"{color}{record.levelname}{self.RESET}" |
| | return super().format(record) |
| |
|
| |
|
| | def setup_logging() -> None: |
| | """Setup logging configuration based on settings.""" |
| | |
| | log_file_path = Path(settings.log_file) |
| | log_file_path.parent.mkdir(parents=True, exist_ok=True) |
| |
|
| | |
| | root_logger = logging.getLogger() |
| | root_logger.setLevel(getattr(logging, settings.log_level)) |
| |
|
| | |
| | root_logger.handlers.clear() |
| |
|
| | |
| | console_handler = logging.StreamHandler(sys.stdout) |
| | console_handler.setLevel(getattr(logging, settings.log_level)) |
| |
|
| | if settings.log_format == "json": |
| | console_formatter = JSONFormatter() |
| | else: |
| | console_formatter = TextFormatter( |
| | fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s", |
| | datefmt="%Y-%m-%d %H:%M:%S" |
| | ) |
| |
|
| | console_handler.setFormatter(console_formatter) |
| | root_logger.addHandler(console_handler) |
| |
|
| | |
| | file_handler = logging.FileHandler(log_file_path, encoding="utf-8") |
| | file_handler.setLevel(getattr(logging, settings.log_level)) |
| | file_handler.setFormatter(JSONFormatter()) |
| | root_logger.addHandler(file_handler) |
| |
|
| | |
| | logging.getLogger("urllib3").setLevel(logging.WARNING) |
| | logging.getLogger("httpx").setLevel(logging.WARNING) |
| | logging.getLogger("httpcore").setLevel(logging.WARNING) |
| | logging.getLogger("asyncio").setLevel(logging.WARNING) |
| |
|
| |
|
| | def get_logger(name: str) -> logging.Logger: |
| | """Get a logger instance with the given name. |
| | |
| | Args: |
| | name: Logger name (typically __name__) |
| | |
| | Returns: |
| | Logger instance |
| | """ |
| | return logging.getLogger(name) |
| |
|