"""봇 감지 이력 기록 CRUD — '몇 번째 요청/어떤 포트에서 감지됐나'를 축적(패턴 분석용).""" from sqlalchemy import text from common.database.db_session_manager import DB_SESSION_MNG from common.enums import DBType, DBWRType class BotDetectionLog: DB = DBType.MAIN.value async def record(self, event: dict): """감지 이벤트 1건 저장. 로깅 실패가 검색을 막지 않도록 호출부에서 예외를 삼킨다.""" sql = text(""" INSERT INTO bot_detection (source, query, ip_request_no, proxy_port, elapsed_sec, marker, headless, html_len) VALUES (:source, :query, :ip_request_no, :proxy_port, :elapsed_sec, :marker, :headless, :html_len) """) params = {k: event.get(k) for k in ("source", "query", "ip_request_no", "proxy_port", "elapsed_sec", "marker", "headless", "html_len")} s = await DB_SESSION_MNG.start_session(self.DB, DBWRType.DB_WRITE.value) try: await s.execute(sql, params) await s.commit() except Exception: await s.rollback() raise finally: await DB_SESSION_MNG.end_session(self.DB, DBWRType.DB_WRITE.value) async def admin_stats(self, hours: int = 168) -> dict: """관리자 FE 용 차단 통계 — 시간대별 건수 + 최근 감지 목록.""" p = {"h": hours} hourly = text(""" SELECT date_trunc('hour', created_at) AS bucket, count(*) AS n FROM bot_detection WHERE created_at > now() - make_interval(hours => :h) GROUP BY 1 ORDER BY 1 """) recent = text(""" SELECT source, query, ip_request_no, proxy_port, elapsed_sec, marker, html_len, created_at FROM bot_detection WHERE created_at > now() - make_interval(hours => :h) ORDER BY created_at DESC LIMIT 50 """) s = await DB_SESSION_MNG.start_session(self.DB, DBWRType.DB_READ.value) try: return { "hourly": [{"bucket": r[0].isoformat(), "count": int(r[1])} for r in (await s.execute(hourly, p)).all()], "items": [dict(r) for r in (await s.execute(recent, p)).mappings().all()], } finally: await DB_SESSION_MNG.end_session(self.DB, DBWRType.DB_READ.value) async def recent_count_by_marker(self, markers: list[str], minutes: int = 60) -> int: """특정 마커의 최근 감지 건수 — '회전으로 못 고치는' 차단만 따로 세어 알림에 쓴다. (일반 차단은 IP 평판 문제라 자연 회복되지만, 구조적 차단은 사람이 설정을 고쳐야 한다)""" if not markers: return 0 sql = text("""SELECT count(*) FROM bot_detection WHERE created_at > now() - make_interval(mins => :m) AND marker = ANY(:mk)""") s = await DB_SESSION_MNG.start_session(self.DB, DBWRType.DB_READ.value) try: return int((await s.execute(sql, {"m": minutes, "mk": list(markers)})).scalar() or 0) finally: await DB_SESSION_MNG.end_session(self.DB, DBWRType.DB_READ.value) async def recent_count(self, minutes: int = 60) -> int: """최근 N분간 봇 감지(차단) 건수 — 차단율 급증 알림·모니터링용.""" sql = text("SELECT count(*) FROM bot_detection WHERE created_at > now() - make_interval(mins => :m)") s = await DB_SESSION_MNG.start_session(self.DB, DBWRType.DB_READ.value) try: return int((await s.execute(sql, {"m": minutes})).scalar() or 0) finally: await DB_SESSION_MNG.end_session(self.DB, DBWRType.DB_READ.value)