mirror of
https://github.com/nethunterzist/trendyol-analiz
synced 2026-07-02 01:47:04 +00:00
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>
This commit is contained in:
197
backend/logging_config.py
Normal file
197
backend/logging_config.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
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},
|
||||
)
|
||||
Reference in New Issue
Block a user