Files
trendyol-analiz/backend/logging_config.py
furkanyigit34 ce1dc1e25f feat: tek birleştirilmiş JSON yapısına geçiş + sosyal kanıt fallback
Ne yaptık:
- data_consolidator.py: Tüm normalizasyon ve hesaplama mantığını main.py'den çıkardık
- Dashboard endpoint 1150 satırdan 25 satıra düştü (main.py -1730/+1880 net)
- Enrichment bitince otomatik konsolide dosya oluşturuluyor (report_{id}_data.json)
- Eski raporlar ilk dashboard isteğinde lazy migration ile konsolide ediliyor
- Trendyol API artık order-count döndürmediği için baskets fallback eklendi
- Inline socialProofs (scrape) > enrichment API öncelik sırası uygulandı
- Frontend KPI başlıkları orders/baskets durumuna göre dinamik değişiyor
- logging_config.py, category_seeder.py, alembic migration eklendi
- Playwright ile 9 tab test edildi, tüm veriler doğru

Neden yaptık:
- 3 farklı kaynaktan her istekte birleştirme yapılması veri tutarsızlığına ve yavaşlığa yol açıyordu
- Tek konsolide JSON dosyası ile dashboard anında yükleniyor
- Trendyol API değişikliği nedeniyle sipariş verisi kayboluyordu, baskets fallback ile çözüldü

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:25:25 +03:00

198 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 = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "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},
)