mirror of
https://github.com/nethunterzist/trendyol-analiz
synced 2026-07-01 09:27:03 +00:00
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>
198 lines
6.3 KiB
Python
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},
|
|
)
|