Files
trendyol-analiz/backend/logging_config.py
furkanyigit34 942c8d1244 fix: scraper Search API fallback + logging + auth
Ne yaptık:
- queue_worker.py: TrendyolSearchScraper 0 ürün döndürdüğünde TrendyolScraper
  (Top Rankings API) ile fallback yap — abiye gibi kategoriler için kritik
- logging_config.py: varsayılan log dizinini /tmp/logs olarak değiştir,
  container restart'ta /logs permission hatası düzeldi
- main.py: API_KEY env var yoksa auth'u gerçekten atla (uyarıyla uyumlu hale getir)

Neden yaptık:
- TrendyolSearchScraper pathModel ile bazı kategoriler (abiye-elbise gibi)
  0 ürün döndürüyor; eski Top Rankings API categoryId ile çalışıyor
- /logs dizini container restart'ta izin hatası veriyordu
- API_KEY yoksa tüm istekler 401 dönüyordu (yorum ile çelişki)
2026-04-15 01:53:45 +03:00

202 lines
6.3 KiB
Python

"""
Structured Logging Configuration for Trendyol Product Dashboard
Provides:
- JSON structured logs to file (for machine parsing)
- Colored console logs (for human reading)
- Correlation ID tracking per request/report
- Rotating file handlers with size limits
- Timing context manager for operation profiling
"""
import logging
import logging.handlers
import json
import os
import time
from contextvars import ContextVar
from contextlib import contextmanager
from datetime import datetime, timezone
# ---------------------------------------------------------------------------
# Context variables for log correlation
# ---------------------------------------------------------------------------
_correlation_id: ContextVar[str] = ContextVar("correlation_id", default="-")
_report_id: ContextVar[str] = ContextVar("report_id", default="-")
def set_correlation_id(cid: str):
_correlation_id.set(cid)
def get_correlation_id() -> str:
return _correlation_id.get()
def set_report_id(rid):
_report_id.set(str(rid) if rid is not None else "-")
def get_report_id() -> str:
return _report_id.get()
# ---------------------------------------------------------------------------
# JSON Formatter (file output)
# ---------------------------------------------------------------------------
class JSONFormatter(logging.Formatter):
"""Structured JSON log formatter for file output."""
def format(self, record: logging.LogRecord) -> str:
log_entry = {
"ts": datetime.now(timezone.utc).isoformat(),
"level": record.levelname,
"logger": record.name,
"msg": record.getMessage(),
"correlation_id": get_correlation_id(),
"report_id": get_report_id(),
}
# Add extra fields if present
for key in ("url", "status_code", "response_time_ms", "response_size",
"error_type", "duration_ms", "cb_state", "failures",
"batch_size", "product_count", "cache_size"):
val = getattr(record, key, None)
if val is not None:
log_entry[key] = val
# Add exception info
if record.exc_info and record.exc_info[0] is not None:
log_entry["exception"] = self.formatException(record.exc_info)
return json.dumps(log_entry, ensure_ascii=False, default=str)
# ---------------------------------------------------------------------------
# Console Formatter (colored, human-readable)
# ---------------------------------------------------------------------------
_LEVEL_COLORS = {
"DEBUG": "\033[36m", # cyan
"INFO": "\033[32m", # green
"WARNING": "\033[33m", # yellow
"ERROR": "\033[31m", # red
"CRITICAL": "\033[1;31m", # bold red
}
_RESET = "\033[0m"
class ConsoleFormatter(logging.Formatter):
"""Colored, human-readable console formatter."""
def format(self, record: logging.LogRecord) -> str:
color = _LEVEL_COLORS.get(record.levelname, "")
ts = datetime.now().strftime("%H:%M:%S")
level = record.levelname[0] # D, I, W, E, C
report = get_report_id()
report_tag = f" [r:{report}]" if report != "-" else ""
msg = record.getMessage()
base = f"{color}{ts} [{level}]{report_tag} {msg}{_RESET}"
if record.exc_info and record.exc_info[0] is not None:
base += "\n" + self.formatException(record.exc_info)
return base
# ---------------------------------------------------------------------------
# Setup function
# ---------------------------------------------------------------------------
def setup_logging(log_dir: str = None):
"""
Configure the entire logging system. Call once at startup.
Creates:
- logs/trendyol.log (all levels, JSON, 10MB x 5 rotation)
- logs/errors.log (WARNING+, JSON, 10MB x 3 rotation)
- console output (INFO+, colored)
"""
if log_dir is None:
log_dir = "/tmp/logs"
try:
os.makedirs(log_dir, exist_ok=True)
except PermissionError:
log_dir = "/tmp/logs"
os.makedirs(log_dir, exist_ok=True)
root = logging.getLogger("trendyol")
root.setLevel(logging.DEBUG)
# Prevent duplicate handlers on reload
if root.handlers:
return
json_fmt = JSONFormatter()
console_fmt = ConsoleFormatter()
# 1. Main log file — all levels, JSON
main_handler = logging.handlers.RotatingFileHandler(
os.path.join(log_dir, "trendyol.log"),
maxBytes=10 * 1024 * 1024, # 10 MB
backupCount=5,
encoding="utf-8",
)
main_handler.setLevel(logging.DEBUG)
main_handler.setFormatter(json_fmt)
root.addHandler(main_handler)
# 2. Error log file — WARNING+, JSON
error_handler = logging.handlers.RotatingFileHandler(
os.path.join(log_dir, "errors.log"),
maxBytes=10 * 1024 * 1024,
backupCount=3,
encoding="utf-8",
)
error_handler.setLevel(logging.WARNING)
error_handler.setFormatter(json_fmt)
root.addHandler(error_handler)
# 3. Console — INFO+, colored
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(console_fmt)
root.addHandler(console_handler)
# Quiet noisy libraries
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("sqlalchemy").setLevel(logging.WARNING)
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
# ---------------------------------------------------------------------------
# Logger factory
# ---------------------------------------------------------------------------
def get_logger(name: str) -> logging.Logger:
"""Get a namespaced logger: trendyol.<name>"""
return logging.getLogger(f"trendyol.{name}")
# ---------------------------------------------------------------------------
# Timing context manager
# ---------------------------------------------------------------------------
@contextmanager
def log_timing(logger: logging.Logger, operation: str, level=logging.INFO, **extra):
"""Context manager that logs operation duration."""
start = time.monotonic()
try:
yield
finally:
elapsed_ms = round((time.monotonic() - start) * 1000, 1)
logger.log(
level,
f"{operation} completed in {elapsed_ms}ms",
extra={"duration_ms": elapsed_ms, **extra},
)