프로덕션 운영 가시성. 행/좀비 워커 감지 + 큐/차단 지표 노출 + 임계 알림. - API: /readyz(DB 도달성=readiness, 실패 503; /healthz=liveness와 구분). /v1/lps/ops(플랫 JSON): 큐 카운트 + oldest_pending_sec(큐지연) + dead_1h + stuck_running + blocks_1h. - crud: JobQueue.ops()/ping(), BotDetectionLog.recent_count(). - worker: run_ops_monitor — 하트비트 파일 주기 갱신(Docker HEALTHCHECK 가 신선도로 행 워커 감지) + 임계(DEAD/차단/큐지연/stuck) 초과 시 WARN 로그 + (LPS_ALERT_WEBHOOK 있으면) Slack 호환 웹훅. - Dockerfile.worker: HEALTHCHECK(하트비트 <120s). 임계·웹훅은 env(LPS_ALERT_*). - 테스트: readyz/ops 2종. 검증: 컨테이너 healthy 판정, 하트비트 갱신, ops 스냅샷 정상. 91 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
38 lines
1.7 KiB
Python
38 lines
1.7 KiB
Python
"""봇 감지 이력 기록 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 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)
|