138 lines
4.7 KiB
Python
138 lines
4.7 KiB
Python
"""로거 - SQLite DB + structlog 기반 작업 이력 추적."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import structlog
|
|
from sqlalchemy import Column, DateTime, Float, Integer, String, Text, create_engine
|
|
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
|
|
|
|
|
# ──────────────────────────── ORM 모델 ────────────────────────────
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
class JobRecord(Base):
|
|
"""조감도 생성 작업 1건의 이력 레코드."""
|
|
__tablename__ = "jobs"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
dxf_path = Column(String(512), nullable=False)
|
|
dxf_hash = Column(String(32))
|
|
timestamp = Column(DateTime, default=datetime.utcnow)
|
|
seed = Column(Integer)
|
|
prompt_version = Column(String(32))
|
|
prompt_hash = Column(String(32))
|
|
status = Column(String(16), default="pending") # pending / running / done / failed
|
|
output_path = Column(String(512))
|
|
quality_score = Column(Float)
|
|
error_message = Column(Text)
|
|
latency_ms = Column(Float)
|
|
|
|
|
|
# ──────────────────────────── DB 세션 ────────────────────────────
|
|
|
|
_engine = None
|
|
_SessionFactory = None
|
|
|
|
|
|
def init_db(db_path: str | Path = "cad_aerial_gen.db"):
|
|
global _engine, _SessionFactory
|
|
_engine = create_engine(f"sqlite:///{db_path}", echo=False)
|
|
Base.metadata.create_all(_engine)
|
|
_SessionFactory = sessionmaker(bind=_engine)
|
|
|
|
|
|
def get_db_session() -> Session:
|
|
if _SessionFactory is None:
|
|
init_db()
|
|
return _SessionFactory()
|
|
|
|
|
|
# ──────────────────────────── structlog 설정 ────────────────────────────
|
|
|
|
def setup_logging(log_file: Optional[Path] = None, level: str = "INFO"):
|
|
"""콘솔 + 파일 동시 로깅을 설정한다."""
|
|
handlers = [logging.StreamHandler(sys.stdout)]
|
|
if log_file:
|
|
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
handlers.append(logging.FileHandler(str(log_file), encoding="utf-8"))
|
|
|
|
logging.basicConfig(
|
|
format="%(message)s",
|
|
level=getattr(logging, level.upper(), logging.INFO),
|
|
handlers=handlers,
|
|
)
|
|
|
|
structlog.configure(
|
|
processors=[
|
|
structlog.contextvars.merge_contextvars,
|
|
structlog.processors.add_log_level,
|
|
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
|
|
structlog.dev.ConsoleRenderer(),
|
|
],
|
|
wrapper_class=structlog.make_filtering_bound_logger(
|
|
getattr(logging, level.upper(), logging.INFO)
|
|
),
|
|
logger_factory=structlog.PrintLoggerFactory(),
|
|
)
|
|
|
|
|
|
def get_logger(name: str = "cad_aerial_gen"):
|
|
return structlog.get_logger(name)
|
|
|
|
|
|
# ──────────────────────────── 작업 이력 헬퍼 ────────────────────────────
|
|
|
|
class JobLogger:
|
|
"""작업 이력 CRUD 래퍼."""
|
|
|
|
def create_job(self, db: Session, dxf_path: str, dxf_hash: str = "") -> JobRecord:
|
|
record = JobRecord(dxf_path=dxf_path, dxf_hash=dxf_hash, status="pending")
|
|
db.add(record)
|
|
db.commit()
|
|
db.refresh(record)
|
|
return record
|
|
|
|
def start_job(self, db: Session, job_id: int, seed: int, prompt_version: str, prompt_hash: str):
|
|
record = db.query(JobRecord).filter_by(id=job_id).first()
|
|
if record:
|
|
record.status = "running"
|
|
record.seed = seed
|
|
record.prompt_version = prompt_version
|
|
record.prompt_hash = prompt_hash
|
|
db.commit()
|
|
|
|
def complete_job(
|
|
self,
|
|
db: Session,
|
|
job_id: int,
|
|
output_path: str,
|
|
quality_score: float,
|
|
latency_ms: float,
|
|
):
|
|
record = db.query(JobRecord).filter_by(id=job_id).first()
|
|
if record:
|
|
record.status = "done"
|
|
record.output_path = output_path
|
|
record.quality_score = quality_score
|
|
record.latency_ms = latency_ms
|
|
db.commit()
|
|
|
|
def fail_job(self, db: Session, job_id: int, error: str):
|
|
record = db.query(JobRecord).filter_by(id=job_id).first()
|
|
if record:
|
|
record.status = "failed"
|
|
record.error_message = error
|
|
db.commit()
|
|
|
|
def list_jobs(self, db: Session, limit: int = 50):
|
|
return db.query(JobRecord).order_by(JobRecord.id.desc()).limit(limit).all()
|